Isolate template authoring validation

This commit is contained in:
tmk241
2026-09-01 23:14:12 +02:00
parent 5b29100340
commit 42710354a6
6 changed files with 388 additions and 307 deletions
+2
View File
@@ -5,3 +5,5 @@ protocol/001 Ad hoc browser commands create ambiguous ordering and compatibility
web/001 Framework abstractions often replace native web semantics unnecessarily People using Hemx applications Retain semantic HTML, accessibility, URLs, forms, and browser fallback
portable/001 Host-only parsing dependencies prevent portable server-side rendering Rust applications targeting Wasm and constrained servers Render through normal Hemx APIs without host parser dependencies
scope/001 Universal extension systems turn a small interaction layer into a client framework Maintainers and application authors Compose ordinary behavior from a small kernel, direct adapters, and explicit islands
maintainability/001 Build and adapter responsibilities are interleaved, making safe changes costly Hemx maintainers and downstream users relying on stable generated contracts Cohesive private ownership seams preserve behavior, diagnostics, and portability
assurance/001 Boundary changes can pass broad suites without focused semantic drift checks Maintainers changing generated APIs, handlers, adapters, and browser runtime Focused checks expose contract and integration drift at the owning seam
1 ID PROBLEM FOR OUTCOME
5 web/001 Framework abstractions often replace native web semantics unnecessarily People using Hemx applications Retain semantic HTML, accessibility, URLs, forms, and browser fallback
6 portable/001 Host-only parsing dependencies prevent portable server-side rendering Rust applications targeting Wasm and constrained servers Render through normal Hemx APIs without host parser dependencies
7 scope/001 Universal extension systems turn a small interaction layer into a client framework Maintainers and application authors Compose ordinary behavior from a small kernel, direct adapters, and explicit islands
8 maintainability/001 Build and adapter responsibilities are interleaved, making safe changes costly Hemx maintainers and downstream users relying on stable generated contracts Cohesive private ownership seams preserve behavior, diagnostics, and portability
9 assurance/001 Boundary changes can pass broad suites without focused semantic drift checks Maintainers changing generated APIs, handlers, adapters, and browser runtime Focused checks expose contract and integration drift at the owning seam
+54 -6
View File
@@ -1,9 +1,57 @@
# Current plan
# PLAN
No live Hemx slices remain.
Current outcome: make Hemx internals easier to change without altering public behavior, generated contracts, diagnostics, or portability.
## Closed receipt — HMX-001–004
## HMX-M01 — Isolate template authoring validation
Result: The closed typed effect path, native interaction recovery, root-owned adapters, deterministic generation, and portable server build are delivered.
Proof: `redgate check`, focused requirement checks, workspace format/clippy/tests/license gates, Wasm target/tree checks, archive checks, and fresh-context reviews passed.
Residual risk: Browser behavior is deterministically checked at generated-markup, runtime, and Axum boundaries rather than through an external browser harness.
Outcome: template authoring validation is privately owned without public, diagnostic, ordering, or fingerprint drift.
Delta: architecture/001 assurance/001
Checks: `cargo test -p hemx-build`; focused contract/fingerprint/diagnostic checks; Redgate; diff check; fresh-context review.
State: Done
Blocked by: none
## HMX-M02 — Isolate Rust source fact extraction
Outcome: `hemx-build` Rust/syn fact collection has one private owner independent of template validation and emission.
Delta: architecture/002 assurance/001
Path: application Rust source -> syntax facts -> generated component/form/handler contract -> deterministic artifact.
Build: move the whole fact model, syn traversal, and related tests together; retain the existing host-only dependency boundary.
Risk: reordered facts or changed path handling alters generated contracts or fingerprints.
Checks: `cargo test -p hemx-build`; generated-contract semantic and fingerprint fixtures; Wasm target tree gate; `redgate check`.
Non-goals: replacing syn, changing generated vocabulary, or introducing a generic analysis framework.
Residual risk: overlaps `hemx-build/src/lib.rs`; implement after HMX-M01 to avoid conflicting movement.
State: Draft
Blocked by: HMX-M01
## HMX-M03 — Isolate artifact emission and diagnostics
Outcome: generated Rust, metadata, lowering tables, and contract diagnostics are emitted through one private boundary behind the public builder.
Delta: architecture/003 assurance/001
Path: validated template and Rust facts -> stable IDs/metadata -> generated files -> downstream compilation diagnostics.
Build: move artifact assembly and diagnostic formatting as one responsibility; keep public entry points and no-op write behavior in place.
Risk: byte, ordering, fingerprint, or diagnostic drift breaks downstream builds despite compiling locally.
Checks: `cargo test -p hemx-build`; exact generated API/diagnostic/fingerprint fixtures; no-op rewrite check; package archive checks; `redgate check`.
Non-goals: a new IR, new serialization, public API changes, or formatting-only rewrites.
Residual risk: this is the broadest movement slice and requires fresh-context review after integrated proof.
State: Draft
Blocked by: HMX-M02
## HMX-M04 — Localize Axum interaction forms
Outcome: media-type enforcement, body limits, URL-encoded/multipart extraction, typed decoding, and rejections have one private Axum owner.
Delta: architecture/004 assurance/002
Path: HTTP request -> `InteractionRequest` extraction -> typed `Form<T>` or custom multipart model -> registered handler/rejection.
Build: move the complete form boundary with its tests; add compile coverage for documented `Form<T>` spellings and custom `FromInteractionForm` extraction.
Risk: extraction order, limits, rejection status/body, or public adapter signatures change.
Checks: `cargo test -p hemx-axum`; `cargo test -p hemx-derive`; focused URL-encoded, multipart, limit, rejection, and compile-pass/fail assertions; `redgate check`.
Non-goals: framework-owned CSRF/auth policy, a general extractor abstraction, or changing native form semantics.
Residual risk: none beyond the existing application-owned security policy boundary after focused route proof.
State: Ready
Blocked by: none
## Closure
Run all repository-required Redgate, fmt, clippy, workspace test, license, Wasm graph, and package archive gates. Finish with a fresh-context blocker-only review against INTENT.tsv, SPEC.tsv, this plan, the actual diff, and proof outputs. Keep `hemx-core`, `hemx-js`, `hemx-derive`, and public package boundaries cohesive unless a later validated responsibility seam requires change.
+6
View File
@@ -58,3 +58,9 @@ test/002 A failed HTML update assertion must report both expected and actual eff
test/003 Public test utilities must inspect complete documents with owned HTML structure. web/001
test/004 With Axum support enabled, public test utilities must inspect effect responses through a real router. protocol/001 server/001
test/005 The public process helper must wait for delayed TCP readiness and capture child output on failure. server/001
architecture/001 hemx-build template authoring validation must live in a private module separate from artifact emission. maintainability/001
architecture/002 hemx-build Rust source fact extraction must live in a private module separate from validation and emission. maintainability/001
architecture/003 hemx-build artifact emission and contract diagnostics must live behind the public builder in a private module. maintainability/001
architecture/004 hemx-axum interaction-form extraction and decoding must live in one private module behind public adapter types. maintainability/001
assurance/001 Generated-contract checks must compare resource capabilities, stable fingerprints, and source diagnostics. assurance/001 resource/001
assurance/002 Handler compile checks must cover each documented Form<T> spelling and custom multipart extraction. assurance/001 server/001
1 ID RULE INTENTS
58 test/003 Public test utilities must inspect complete documents with owned HTML structure. web/001
59 test/004 With Axum support enabled, public test utilities must inspect effect responses through a real router. protocol/001 server/001
60 test/005 The public process helper must wait for delayed TCP readiness and capture child output on failure. server/001
61 architecture/001 hemx-build template authoring validation must live in a private module separate from artifact emission. maintainability/001
62 architecture/002 hemx-build Rust source fact extraction must live in a private module separate from validation and emission. maintainability/001
63 architecture/003 hemx-build artifact emission and contract diagnostics must live behind the public builder in a private module. maintainability/001
64 architecture/004 hemx-axum interaction-form extraction and decoding must live in one private module behind public adapter types. maintainability/001
65 assurance/001 Generated-contract checks must compare resource capabilities, stable fingerprints, and source diagnostics. assurance/001 resource/001
66 assurance/002 Handler compile checks must cover each documented Form<T> spelling and custom multipart extraction. assurance/001 server/001
+314
View File
@@ -0,0 +1,314 @@
use super::*;
pub(super) fn reject_selector_target_attrs(
path: &Path,
attrs: &[SurfaceAttribute],
) -> io::Result<()> {
for attr in attrs {
let name = attr.name.as_str();
if matches!(name, "data-hemx-target" | "data-hemx-select") {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: `{name}` is selector-style targeting; hemx uses generated resources instead. Add data-hemx-slot to the local element and return an effect for that generated slot.",
path.display()
),
));
}
}
Ok(())
}
pub(super) fn reject_unknown_hemx_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
for attr in attrs {
let name = attr.name.as_str();
if name.starts_with("data-hemx-") && !known_hemx_attr(name) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: unknown hemx attribute `{name}`; check the spelling or use a non-hemx data-* attribute for app-specific metadata",
path.display()
),
));
}
}
Ok(())
}
pub(super) fn known_hemx_attr(name: &str) -> bool {
matches!(
name,
"data-hemx-root"
| "data-hemx-sse"
| "data-hemx-ws"
| "data-hemx-st"
| "data-hemx-handle"
| "data-hemx-slot"
| "data-hemx-form"
| "data-hemx-atom"
| "data-hemx-key"
| "data-hemx-on"
| "data-hemx-pending-class"
| "data-hemx-indicator"
| "data-hemx-confirm"
| "data-hemx-debounce"
| "data-hemx-throttle"
| "data-hemx-every"
| "data-hemx-interval"
| "data-hemx-revealed"
| "data-hemx-revealed-ahead"
| "data-hemx-disable-while-pending"
| "data-hemx-policy"
| "data-hemx-nav"
| "data-hemx-history"
| "data-hemx-boost"
| "data-hemx-error-for"
| "data-hemx-error"
| "data-hemx-island"
)
}
pub(super) fn reject_invalid_hemx_attr_values(
path: &Path,
attrs: &[SurfaceAttribute],
) -> io::Result<()> {
for attr in attrs
.iter()
.filter(|attr| attr.origin == AttributeOrigin::Static)
{
let value = attr.value.as_deref().unwrap_or("");
match attr.name.as_str() {
"data-hemx-policy" if !valid_policy(value) => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected one of `latest`, `queue`, `drop`, or `parallel`",
));
}
"data-hemx-history" if !matches!(value.trim(), "" | "push" | "replace") => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected `push`, `replace`, or empty for the default push behavior",
));
}
"data-hemx-on" if !valid_event_list(value) => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected runtime-supported events: `click`, `submit`, `input`, `change`, `keydown`, `dragstart`, `dragover`, or `drop`",
));
}
"data-hemx-confirm" if value.trim().is_empty() => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected a non-empty confirmation message",
));
}
"data-hemx-sse" if value.trim().is_empty() => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected a non-empty same-origin SSE URL",
));
}
"data-hemx-ws" if value.trim().is_empty() => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected a non-empty same-origin WebSocket URL",
));
}
"data-hemx-revealed-ahead"
if !value
.trim()
.parse::<f64>()
.ok()
.is_some_and(|value| value.is_finite() && value >= 0.0) =>
{
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected a non-negative number of viewports",
));
}
"data-hemx-debounce" | "data-hemx-throttle" | "data-hemx-every"
| "data-hemx-interval"
if !valid_duration(value) =>
{
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected milliseconds like `250`/`250ms` or seconds like `1s`",
));
}
_ => {}
}
}
Ok(())
}
pub(super) fn reject_invalid_hemx_attr_placement(
path: &Path,
tag: &str,
attrs: &[SurfaceAttribute],
) -> io::Result<()> {
if has_attr(attrs, "data-hemx-nav")
&& (tag != "a"
|| !has_attr(attrs, "href")
|| static_attr(attrs, "href").is_some_and(|href| href.trim().is_empty()))
{
return Err(invalid_hemx_placement(
path,
"data-hemx-nav",
"expected a real `<a href=...>` link so navigation works without JavaScript",
));
}
if has_attr(attrs, "data-hemx-boost") && matches!(tag, "a" | "form") {
return Err(invalid_hemx_placement(
path,
"data-hemx-boost",
"expected a container around descendant links/forms; use `data-hemx-nav` on anchors or `data-hemx-handle` on forms",
));
}
if has_attr(attrs, "data-hemx-sse") && !has_attr(attrs, "data-hemx-root") {
return Err(invalid_hemx_placement(
path,
"data-hemx-sse",
"expected placement on the same element as `data-hemx-root`",
));
}
if has_attr(attrs, "data-hemx-ws") && !has_attr(attrs, "data-hemx-root") {
return Err(invalid_hemx_placement(
path,
"data-hemx-ws",
"expected placement on the same element as `data-hemx-root`",
));
}
Ok(())
}
fn has_attr(attrs: &[SurfaceAttribute], name: &str) -> bool {
attrs.iter().any(|attr| attr.name == name)
}
fn invalid_hemx_placement(path: &Path, attr: &str, expectation: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: invalid {attr} placement; {expectation}",
path.display()
),
)
}
fn valid_policy(value: &str) -> bool {
matches!(value.trim(), "latest" | "queue" | "drop" | "parallel")
}
fn valid_event_list(value: &str) -> bool {
let mut events = event_tokens(value).peekable();
events.peek().is_some() && events.all(valid_runtime_event)
}
fn valid_runtime_event(value: &str) -> bool {
matches!(
value,
"click" | "submit" | "input" | "change" | "keydown" | "dragstart" | "dragover" | "drop"
)
}
fn valid_duration(value: &str) -> bool {
let value = value.trim();
let digits = value
.strip_suffix("ms")
.or_else(|| value.strip_suffix('s'))
.unwrap_or(value);
!digits.is_empty() && digits.as_bytes().iter().all(u8::is_ascii_digit)
}
fn invalid_hemx_value(path: &Path, attr: &str, value: &str, expectation: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: invalid {attr} value `{value}`; {expectation}",
path.display()
),
)
}
pub(super) fn reject_unkeyed_loop(
surface: &SurfaceDocument,
scope: ScopeId,
path: &Path,
kind: &str,
name: &str,
) -> io::Result<()> {
if let Some(diagnostic) =
unkeyed_generated_target_diagnostic_for_scope(surface, scope, path, kind, name)
{
Err(diagnostic.to_io_error())
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn static_attribute_value_validators_cover_every_boundary() {
for valid in ["latest", " queue ", "drop", "parallel"] {
assert!(valid_policy(valid), "rejected policy {valid:?}");
}
for invalid in ["", "newest", "latest queue", "LATEST"] {
assert!(!valid_policy(invalid), "accepted policy {invalid:?}");
}
for valid in [
"click",
"submit input change keydown dragstart dragover drop",
" click change ",
] {
assert!(valid_event_list(valid), "rejected event list {valid:?}");
}
for invalid in ["", " ", "blur", "click blur"] {
assert!(
!valid_event_list(invalid),
"accepted event list {invalid:?}"
);
}
for valid in [
"click",
"submit",
"input",
"change",
"keydown",
"dragstart",
"dragover",
"drop",
] {
assert!(valid_runtime_event(valid), "rejected event {valid:?}");
}
for invalid in ["", "blur", "Click"] {
assert!(!valid_runtime_event(invalid), "accepted event {invalid:?}");
}
for valid in ["0", "250", "250ms", "1s", " 5s "] {
assert!(valid_duration(valid), "rejected duration {valid:?}");
}
for invalid in ["", " ", "ms", "s", "-1", "1sec", "1.5s", "1 ms"] {
assert!(!valid_duration(invalid), "accepted duration {invalid:?}");
}
}
}
+8 -300
View File
@@ -10,6 +10,13 @@ use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
mod authoring;
use authoring::{
reject_invalid_hemx_attr_placement, reject_invalid_hemx_attr_values,
reject_selector_target_attrs, reject_unkeyed_loop, reject_unknown_hemx_attrs,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiagnosticSeverity {
Error,
@@ -1487,262 +1494,6 @@ fn is_inside_keyed_for(surface: &SurfaceDocument, mut scope: ScopeId) -> bool {
}
}
fn reject_selector_target_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
for attr in attrs {
let name = attr.name.as_str();
if matches!(name, "data-hemx-target" | "data-hemx-select") {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: `{name}` is selector-style targeting; hemx uses generated resources instead. Add data-hemx-slot to the local element and return an effect for that generated slot.",
path.display()
),
));
}
}
Ok(())
}
fn reject_unknown_hemx_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
for attr in attrs {
let name = attr.name.as_str();
if name.starts_with("data-hemx-") && !known_hemx_attr(name) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: unknown hemx attribute `{name}`; check the spelling or use a non-hemx data-* attribute for app-specific metadata",
path.display()
),
));
}
}
Ok(())
}
fn known_hemx_attr(name: &str) -> bool {
matches!(
name,
"data-hemx-root"
| "data-hemx-sse"
| "data-hemx-ws"
| "data-hemx-st"
| "data-hemx-handle"
| "data-hemx-slot"
| "data-hemx-form"
| "data-hemx-atom"
| "data-hemx-key"
| "data-hemx-on"
| "data-hemx-pending-class"
| "data-hemx-indicator"
| "data-hemx-confirm"
| "data-hemx-debounce"
| "data-hemx-throttle"
| "data-hemx-every"
| "data-hemx-interval"
| "data-hemx-revealed"
| "data-hemx-revealed-ahead"
| "data-hemx-disable-while-pending"
| "data-hemx-policy"
| "data-hemx-nav"
| "data-hemx-history"
| "data-hemx-boost"
| "data-hemx-error-for"
| "data-hemx-error"
| "data-hemx-island"
)
}
fn reject_invalid_hemx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
for attr in attrs
.iter()
.filter(|attr| attr.origin == AttributeOrigin::Static)
{
let value = attr.value.as_deref().unwrap_or("");
match attr.name.as_str() {
"data-hemx-policy" if !valid_policy(value) => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected one of `latest`, `queue`, `drop`, or `parallel`",
));
}
"data-hemx-history" if !matches!(value.trim(), "" | "push" | "replace") => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected `push`, `replace`, or empty for the default push behavior",
));
}
"data-hemx-on" if !valid_event_list(value) => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected runtime-supported events: `click`, `submit`, `input`, `change`, `keydown`, `dragstart`, `dragover`, or `drop`",
));
}
"data-hemx-confirm" if value.trim().is_empty() => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected a non-empty confirmation message",
));
}
"data-hemx-sse" if value.trim().is_empty() => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected a non-empty same-origin SSE URL",
));
}
"data-hemx-ws" if value.trim().is_empty() => {
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected a non-empty same-origin WebSocket URL",
));
}
"data-hemx-revealed-ahead"
if !value
.trim()
.parse::<f64>()
.ok()
.is_some_and(|value| value.is_finite() && value >= 0.0) =>
{
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected a non-negative number of viewports",
));
}
"data-hemx-debounce" | "data-hemx-throttle" | "data-hemx-every"
| "data-hemx-interval"
if !valid_duration(value) =>
{
return Err(invalid_hemx_value(
path,
&attr.name,
value,
"expected milliseconds like `250`/`250ms` or seconds like `1s`",
));
}
_ => {}
}
}
Ok(())
}
fn reject_invalid_hemx_attr_placement(
path: &Path,
tag: &str,
attrs: &[SurfaceAttribute],
) -> io::Result<()> {
if has_attr(attrs, "data-hemx-nav")
&& (tag != "a"
|| !has_attr(attrs, "href")
|| static_attr(attrs, "href").is_some_and(|href| href.trim().is_empty()))
{
return Err(invalid_hemx_placement(
path,
"data-hemx-nav",
"expected a real `<a href=...>` link so navigation works without JavaScript",
));
}
if has_attr(attrs, "data-hemx-boost") && matches!(tag, "a" | "form") {
return Err(invalid_hemx_placement(
path,
"data-hemx-boost",
"expected a container around descendant links/forms; use `data-hemx-nav` on anchors or `data-hemx-handle` on forms",
));
}
if has_attr(attrs, "data-hemx-sse") && !has_attr(attrs, "data-hemx-root") {
return Err(invalid_hemx_placement(
path,
"data-hemx-sse",
"expected placement on the same element as `data-hemx-root`",
));
}
if has_attr(attrs, "data-hemx-ws") && !has_attr(attrs, "data-hemx-root") {
return Err(invalid_hemx_placement(
path,
"data-hemx-ws",
"expected placement on the same element as `data-hemx-root`",
));
}
Ok(())
}
fn has_attr(attrs: &[SurfaceAttribute], name: &str) -> bool {
attrs.iter().any(|attr| attr.name == name)
}
fn invalid_hemx_placement(path: &Path, attr: &str, expectation: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: invalid {attr} placement; {expectation}",
path.display()
),
)
}
fn valid_policy(value: &str) -> bool {
matches!(value.trim(), "latest" | "queue" | "drop" | "parallel")
}
fn valid_event_list(value: &str) -> bool {
let mut events = event_tokens(value).peekable();
events.peek().is_some() && events.all(valid_runtime_event)
}
fn valid_runtime_event(value: &str) -> bool {
matches!(
value,
"click" | "submit" | "input" | "change" | "keydown" | "dragstart" | "dragover" | "drop"
)
}
fn valid_duration(value: &str) -> bool {
let value = value.trim();
let digits = value
.strip_suffix("ms")
.or_else(|| value.strip_suffix('s'))
.unwrap_or(value);
!digits.is_empty() && digits.as_bytes().iter().all(u8::is_ascii_digit)
}
fn invalid_hemx_value(path: &Path, attr: &str, value: &str, expectation: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: invalid {attr} value `{value}`; {expectation}",
path.display()
),
)
}
fn reject_unkeyed_loop(
surface: &SurfaceDocument,
scope: ScopeId,
path: &Path,
kind: &str,
name: &str,
) -> io::Result<()> {
if let Some(diagnostic) =
unkeyed_generated_target_diagnostic_for_scope(surface, scope, path, kind, name)
{
Err(diagnostic.to_io_error())
} else {
Ok(())
}
}
fn context_type_for_heml_path(path: &Path) -> Option<String> {
let stem = path.file_stem()?.to_str()?;
let mut out = String::new();
@@ -4110,50 +3861,7 @@ fn main() {{
}
#[test]
fn static_attribute_value_validators_cover_every_boundary() {
for valid in ["latest", " queue ", "drop", "parallel"] {
assert!(valid_policy(valid), "rejected policy {valid:?}");
}
for invalid in ["", "newest", "latest queue", "LATEST"] {
assert!(!valid_policy(invalid), "accepted policy {invalid:?}");
}
for valid in [
"click",
"submit input change keydown dragstart dragover drop",
" click change ",
] {
assert!(valid_event_list(valid), "rejected event list {valid:?}");
}
for invalid in ["", " ", "blur", "click blur"] {
assert!(
!valid_event_list(invalid),
"accepted event list {invalid:?}"
);
}
for valid in [
"click",
"submit",
"input",
"change",
"keydown",
"dragstart",
"dragover",
"drop",
] {
assert!(valid_runtime_event(valid), "rejected event {valid:?}");
}
for invalid in ["", "blur", "Click"] {
assert!(!valid_runtime_event(invalid), "accepted event {invalid:?}");
}
for valid in ["0", "250", "250ms", "1s", " 5s "] {
assert!(valid_duration(valid), "rejected duration {valid:?}");
}
for invalid in ["", " ", "ms", "s", "-1", "1sec", "1.5s", "1 ms"] {
assert!(!valid_duration(invalid), "accepted duration {invalid:?}");
}
fn static_attribute_values_integrate_with_template_inspection() {
let valid_source = r#"
<main data-hemx-root="app" data-hemx-sse="/events" data-hemx-ws="/room/socket">
<button data-hemx-handle="save" data-hemx-policy="latest" data-hemx-on="click change" data-hemx-confirm="Save?" data-hemx-debounce="250ms" data-hemx-throttle="1s">Save</button>
+4 -1
View File
@@ -11,7 +11,10 @@ cargo test -p hemx-core --test effect_batch generated_resource_helpers_preserve_
cargo test -p hemx-js --test runtime runtime_executes_every_closed_effect_with_owned_targets -- --exact # spec: kernel/006 check # spec: kernel/013 check # spec: kernel/014 check # spec: kernel/015 check # spec: kernel/016 check
cargo test -p hemx-js --test runtime runtime_applies_batches_in_order_and_stops_on_failure -- --exact # spec: kernel/004 check # spec: kernel/005 check # spec: runtime/005 check
cargo test -p hemx-build public_heml_inspection_entry_points_preserve_paths_and_io_diagnostics --lib # spec: build/001 check
cargo test -p hemx-build generated_contract_fingerprint_is_deterministic --lib # spec: build/002 check
cargo test -p hemx-build generated_contract_fingerprint_is_deterministic --lib # spec: build/002 check # spec: assurance/001 check
cargo test -p hemx-build emits_generated_resources_from_heml --lib # spec: assurance/001 check
cargo test -p hemx-build unkeyed_generated_target_diagnostic_is_structured --lib # spec: assurance/001 check
test -f hemx-build/src/authoring.rs && grep -q '^mod authoring;$' hemx-build/src/lib.rs && ! grep -Eq '^fn (reject_selector_target_attrs|reject_unknown_hemx_attrs|known_hemx_attr|reject_invalid_hemx_attr_values|reject_invalid_hemx_attr_placement|reject_unkeyed_loop)\(' hemx-build/src/lib.rs # spec: architecture/001 check
cargo test -p hemx-build no_op_build_preserves_generated_artifact_timestamps --lib # spec: build/003 check # spec: resource/005 check
cargo test -p hemx-build changed_template_refreshes_generated_artifacts --lib # spec: resource/005 check
cargo test -p hemx-derive --test surface surface_macro_preserves_inline_module_with_generated_file -- --exact # spec: derive/001 check