feat(api): streamline generated app authoring
Move the canonical examples toward generated component-root helpers, typed form decoding, async/state handler registration, and derive-driven app/component registry wiring. Tighten requirements and diagnostics for the server-first, selectorless authoring path. Verified with cargo run -p slhx-xtask -- test, cargo check --workspace, redgate list, redgate refs, redgate health --strict, and git diff --check. req: canonical/001 req: canonical/003 req: canonical/004 req: dx/002 req: derive_app/001 req: component/003 req: form/004 req: axum_integration/003
This commit is contained in:
+238
-173
@@ -40,7 +40,7 @@ slhx = {{ path = {:?} }}
|
||||
mod todos {
|
||||
#[slhx::handler]
|
||||
fn create() -> impl slhx::IntoEffect {
|
||||
slhx::EffectBatch::default()
|
||||
slhx::advanced::EffectBatch::default()
|
||||
}
|
||||
}
|
||||
"#,
|
||||
@@ -95,7 +95,7 @@ slhx = {{ path = {:?} }}
|
||||
mod todos {
|
||||
#[slhx::handler]
|
||||
fn create() -> impl slhx::IntoEffect {
|
||||
slhx::event("created", "")
|
||||
slhx::EventName::new("created").emit("")
|
||||
}
|
||||
}
|
||||
"#,
|
||||
@@ -110,6 +110,176 @@ mod todos {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_macro_rejects_handlers_outside_scoped_component() {
|
||||
// req: component/003 req: component/005 req: test/003
|
||||
let fixture = Fixture::new("slhx-derive-component-extra-handler-fail");
|
||||
fixture.write(
|
||||
"Cargo.toml",
|
||||
&format!(
|
||||
r#"[package]
|
||||
name = "slhx-derive-component-extra-handler-fail"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
slhx = {{ path = {:?} }}
|
||||
"#,
|
||||
repo_path("slhx")
|
||||
),
|
||||
);
|
||||
fixture.write(
|
||||
"build.rs",
|
||||
r#"fn main() {
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||
std::fs::write(
|
||||
out.join("slhx.syms"),
|
||||
"slhx-syms-v1\nhandle\ttemplates/todos.heml::create\tcreate\t1\nhandle\ttemplates/admin.heml::delete\tdelete\t2\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
fixture.write(
|
||||
"src/lib.rs",
|
||||
r#"#[slhx::component("todos")]
|
||||
mod handlers {
|
||||
#[slhx::handler]
|
||||
fn create() -> impl slhx::IntoEffect {
|
||||
slhx::EventName::new("created").emit("")
|
||||
}
|
||||
|
||||
#[slhx::handler]
|
||||
fn delete() -> impl slhx::IntoEffect {
|
||||
slhx::EventName::new("deleted").emit("")
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let output = check_fixture(&fixture);
|
||||
|
||||
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("handler(s) not declared by this component's generated handles: delete"),
|
||||
"missing scoped extra-handler diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_macro_rejects_unknown_scoped_component() {
|
||||
// req: component/003 req: component/005 req: test/003
|
||||
let fixture = Fixture::new("slhx-derive-component-unknown-scope-fail");
|
||||
fixture.write(
|
||||
"Cargo.toml",
|
||||
&format!(
|
||||
r#"[package]
|
||||
name = "slhx-derive-component-unknown-scope-fail"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
slhx = {{ path = {:?} }}
|
||||
"#,
|
||||
repo_path("slhx")
|
||||
),
|
||||
);
|
||||
fixture.write(
|
||||
"build.rs",
|
||||
r#"fn main() {
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||
std::fs::write(
|
||||
out.join("slhx.syms"),
|
||||
"slhx-syms-v1\nhandle\ttemplates/admin.heml::create\tcreate\t1\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
fixture.write(
|
||||
"src/lib.rs",
|
||||
r#"#[slhx::component("todos")]
|
||||
mod handlers {
|
||||
#[slhx::handler]
|
||||
fn create() -> impl slhx::IntoEffect {
|
||||
slhx::EventName::new("created").emit("")
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let output = check_fixture(&fixture);
|
||||
|
||||
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("#[slhx::component(\"todos\")] does not match any generated handles"),
|
||||
"missing unknown component diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_macro_rejects_ambiguous_generated_handles() {
|
||||
// req: component/003 req: component/005 req: test/003
|
||||
let fixture = Fixture::new("slhx-derive-component-ambiguous-handle-fail");
|
||||
fixture.write(
|
||||
"Cargo.toml",
|
||||
&format!(
|
||||
r#"[package]
|
||||
name = "slhx-derive-component-ambiguous-handle-fail"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
slhx = {{ path = {:?} }}
|
||||
"#,
|
||||
repo_path("slhx")
|
||||
),
|
||||
);
|
||||
fixture.write(
|
||||
"build.rs",
|
||||
r#"fn main() {
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||
std::fs::write(
|
||||
out.join("slhx.syms"),
|
||||
"slhx-syms-v1\nhandle\ttemplates/todos.heml::save\tsave\t1\nhandle\ttemplates/todos.heml::section::save\tsave\t2\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
fixture.write(
|
||||
"src/lib.rs",
|
||||
r#"#[slhx::component("todos")]
|
||||
mod handlers {
|
||||
#[slhx::handler]
|
||||
fn save() -> impl slhx::IntoEffect {
|
||||
slhx::EventName::new("saved").emit("")
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let output = check_fixture(&fixture);
|
||||
|
||||
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("ambiguous generated handle name(s): save"),
|
||||
"missing ambiguous-handle diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handler_macro_reports_unknown_handle_and_bad_shape() {
|
||||
// req: derive_handler/001 req: test/003
|
||||
@@ -147,7 +317,7 @@ slhx = {{ path = {:?} }}
|
||||
"src/lib.rs",
|
||||
r#"#[slhx::handler]
|
||||
fn missing() -> impl slhx::IntoEffect {
|
||||
slhx::EffectBatch::default()
|
||||
slhx::advanced::EffectBatch::default()
|
||||
}
|
||||
|
||||
#[slhx::handler]
|
||||
@@ -201,7 +371,7 @@ slhx = {{ path = {:?} }}
|
||||
"src/lib.rs",
|
||||
r#"#[slhx::handler]
|
||||
fn create() -> impl slhx::IntoEffect {
|
||||
slhx::EffectBatch::default()
|
||||
slhx::advanced::EffectBatch::default()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
@@ -261,7 +431,7 @@ slhx = {{ path = {:?} }}
|
||||
"src/lib.rs",
|
||||
r#"#[slhx::handler]
|
||||
fn show(todo_id: String) -> impl slhx::IntoEffect {
|
||||
slhx::EffectBatch::default()
|
||||
slhx::advanced::EffectBatch::default()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
@@ -479,7 +649,7 @@ struct CreateTodo {
|
||||
|
||||
#[slhx::handler]
|
||||
fn create(_form: slhx::Form<CreateTodo>) -> impl slhx::IntoEffect {
|
||||
slhx::event("created", "")
|
||||
slhx::EventName::new("created").emit("")
|
||||
}
|
||||
|
||||
fn smoke() {
|
||||
@@ -497,62 +667,6 @@ fn smoke() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_handle_requires_checked_form_model() {
|
||||
// req: form/001 req: form/004 req: form/006 req: test/003
|
||||
let fixture = Fixture::new("slhx-derive-form-handler-unchecked-model-fail");
|
||||
fixture.write(
|
||||
"Cargo.toml",
|
||||
&format!(
|
||||
r#"[package]
|
||||
name = "slhx-derive-form-handler-unchecked-model-fail"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
slhx = {{ path = {:?} }}
|
||||
"#,
|
||||
repo_path("slhx")
|
||||
),
|
||||
);
|
||||
fixture.write(
|
||||
"build.rs",
|
||||
r#"fn main() {
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||
std::fs::write(
|
||||
out.join("slhx.syms"),
|
||||
"slhx-syms-v1\nhandle\ttemplates/app.heml::create\tcreate\t1\nhandle_form\tcreate\tnew_todo\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
fixture.write(
|
||||
"src/lib.rs",
|
||||
r#"struct CreateTodo {
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[slhx::handler]
|
||||
fn create(_form: slhx::Form<CreateTodo>) -> impl slhx::IntoEffect {
|
||||
slhx::event("created", "")
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let output = check_fixture(&fixture);
|
||||
|
||||
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("CreateTodo: FormModel") || stderr.contains("CreateTodo: slhx::FormModel"),
|
||||
"missing checked form model diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_handle_requires_form_parameter() {
|
||||
// req: form/004 req: form/006 req: test/003
|
||||
@@ -590,7 +704,7 @@ slhx = {{ path = {:?} }}
|
||||
"src/lib.rs",
|
||||
r#"#[slhx::handler]
|
||||
fn create() -> impl slhx::IntoEffect {
|
||||
slhx::EffectBatch::default()
|
||||
slhx::advanced::EffectBatch::default()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
@@ -600,11 +714,70 @@ fn create() -> impl slhx::IntoEffect {
|
||||
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("slhx handler `create` handles a generated form and must accept slhx::Form<_>"),
|
||||
stderr.contains(
|
||||
"slhx handler `create` handles a generated form and must accept a typed form argument"
|
||||
),
|
||||
"missing form-handler diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_handle_rejects_state_only_handler() {
|
||||
// req: form/004 req: form/006 req: test/003
|
||||
let fixture = Fixture::new("slhx-derive-form-handler-state-only-fail");
|
||||
fixture.write(
|
||||
"Cargo.toml",
|
||||
&format!(
|
||||
r#"[package]
|
||||
name = "slhx-derive-form-handler-state-only-fail"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
slhx = {{ path = {:?} }}
|
||||
"#,
|
||||
repo_path("slhx")
|
||||
),
|
||||
);
|
||||
fixture.write(
|
||||
"build.rs",
|
||||
r#"fn main() {
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||
std::fs::write(
|
||||
out.join("slhx.syms"),
|
||||
"slhx-syms-v1\nhandle\ttemplates/app.heml::create\tcreate\t1\nhandle_form\tcreate\tnew_todo\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
fixture.write(
|
||||
"src/lib.rs",
|
||||
r#"struct App;
|
||||
struct State<T>(T);
|
||||
|
||||
#[slhx::handler]
|
||||
fn create(_state: State<App>) -> impl slhx::IntoEffect {
|
||||
slhx::advanced::EffectBatch::default()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let output = check_fixture(&fixture);
|
||||
|
||||
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains(
|
||||
"slhx handler `create` handles a generated form and must accept a typed form argument"
|
||||
),
|
||||
"missing state-only form-handler diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_handle_still_requires_generated_param_arguments() {
|
||||
// req: form/006 req: derive_handler/003 req: test/003
|
||||
@@ -644,7 +817,7 @@ slhx = {{ path = {:?} }}
|
||||
|
||||
#[slhx::handler]
|
||||
fn create(_form: slhx::Form<CreateTodo>) -> impl slhx::IntoEffect {
|
||||
slhx::EffectBatch::default()
|
||||
slhx::advanced::EffectBatch::default()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
@@ -659,114 +832,6 @@ fn create(_form: slhx::Form<CreateTodo>) -> impl slhx::IntoEffect {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_handle_rejects_nongeneric_form_impostor() {
|
||||
// req: form/004 req: form/006 req: test/003
|
||||
let fixture = Fixture::new("slhx-derive-form-handler-nongeneric-impostor-fail");
|
||||
fixture.write(
|
||||
"Cargo.toml",
|
||||
&format!(
|
||||
r#"[package]
|
||||
name = "slhx-derive-form-handler-nongeneric-impostor-fail"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
slhx = {{ path = {:?} }}
|
||||
"#,
|
||||
repo_path("slhx")
|
||||
),
|
||||
);
|
||||
fixture.write(
|
||||
"build.rs",
|
||||
r#"fn main() {
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||
std::fs::write(
|
||||
out.join("slhx.syms"),
|
||||
"slhx-syms-v1\nhandle\ttemplates/app.heml::create\tcreate\t1\nhandle_form\tcreate\tnew_todo\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
fixture.write(
|
||||
"src/lib.rs",
|
||||
r#"struct Form;
|
||||
|
||||
#[slhx::handler]
|
||||
fn create(_form: Form) -> impl slhx::IntoEffect {
|
||||
slhx::EffectBatch::default()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let output = check_fixture(&fixture);
|
||||
|
||||
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("slhx handler `create` handles a generated form and must accept slhx::Form<_>"),
|
||||
"missing nongeneric form diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_handle_rejects_form_name_suffix_impostor() {
|
||||
// req: form/004 req: form/006 req: test/003
|
||||
let fixture = Fixture::new("slhx-derive-form-impostor-fail");
|
||||
fixture.write(
|
||||
"Cargo.toml",
|
||||
&format!(
|
||||
r#"[package]
|
||||
name = "slhx-derive-form-impostor-fail"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
slhx = {{ path = {:?} }}
|
||||
"#,
|
||||
repo_path("slhx")
|
||||
),
|
||||
);
|
||||
fixture.write(
|
||||
"build.rs",
|
||||
r#"fn main() {
|
||||
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||
std::fs::write(
|
||||
out.join("slhx.syms"),
|
||||
"slhx-syms-v1\nhandle\ttemplates/app.heml::create\tcreate\t1\nhandle_form\tcreate\tnew_todo\n",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#,
|
||||
);
|
||||
fixture.write(
|
||||
"src/lib.rs",
|
||||
r#"struct CreateForm;
|
||||
|
||||
#[slhx::handler]
|
||||
fn create(_form: CreateForm) -> impl slhx::IntoEffect {
|
||||
slhx::EffectBatch::default()
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let output = check_fixture(&fixture);
|
||||
|
||||
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("slhx handler `create` handles a generated form and must accept slhx::Form<_>"),
|
||||
"missing form-impostor diagnostic in stderr:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_macro_reports_missing_generated_include() {
|
||||
// req: build/004 req: test/003
|
||||
|
||||
Reference in New Issue
Block a user