refactor!: rename slhx to hemx

Rename the tracked product identity, crate/package names, Rust paths/macros, generated artifacts, runtime files, public attributes, examples, docs, requirements, and tests from slhx to hemx without compatibility shims.

Verified with cargo run -p hemx-xtask -- test, cargo test -p hemx-derive --test compile_fail, cargo test -p hemx-js, cargo test -p hemx-axum, cargo test -p hemx-v0-examples, cargo check --workspace, redgate list, redgate refs, redgate health --strict, git diff --check, and git grep/ls-files legacy-name audits.

req: misc/001

req: codegen/001

req: component/004

req: runtime/001
This commit is contained in:
slhx agent
2026-06-05 06:52:37 +02:00
parent d4e865ef92
commit c33500440e
69 changed files with 1415 additions and 1415 deletions
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "hemx-axum"
version.workspace = true
edition.workspace = true
[lib]
path = "src/lib.rs"
[dependencies]
axum = { version = "0.7", default-features = false, features = ["multipart", "tokio"] }
futures-util = { version = "0.3", default-features = false }
hemx-core = { path = "../hemx-core" }
hemx-js = { path = "../hemx-js" }
[dev-dependencies]
scraper = "0.23"
tokio = { version = "1", features = ["macros", "rt"] }
+1358
View File
File diff suppressed because it is too large Load Diff
+410
View File
@@ -0,0 +1,410 @@
use axum::extract::State;
use axum::http::{header, HeaderMap};
use axum::response::IntoResponse;
use scraper::{Html, Selector};
use hemx_axum::{
interactions, runtime_js, DispatchRejection, EffectResponse, Form, InteractionForm,
InteractionFormRejection, InteractionRequest, PageMode, PageRequest, PageResponse,
HEMX_CONTENT_TYPE, HEMX_FINGERPRINT_HEADER, HEMX_PARTIAL_HEADER, HEMX_RUNTIME_CONTENT_TYPE,
HEMX_TITLE_HEADER,
};
use hemx_core::{push, BuildFingerprint, Handle, IntoEffect, SafeHtml, Slot};
fn selector(value: &str) -> Selector {
Selector::parse(value).expect("test selector parses")
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ProjectId(u32);
impl std::str::FromStr for ProjectId {
type Err = std::num::ParseIntError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
value.parse().map(ProjectId)
}
}
struct OpenProject {
project_id: ProjectId,
}
impl hemx_core::FromForm for OpenProject {
fn from_form_fields(fields: &[(String, String)]) -> Result<Self, hemx_core::FormError> {
let Some(value) = fields
.iter()
.find_map(|(name, value)| (name == "project_id").then_some(value.as_str()))
else {
return Err(hemx_core::FormError::new("missing form field `project_id`"));
};
Ok(Self {
project_id: value
.parse()
.map_err(|_| hemx_core::FormError::new("invalid form field `project_id`"))?,
})
}
}
#[test]
fn page_mode_detects_partial_header() {
let mut headers = HeaderMap::new();
assert_eq!(PageMode::from_headers(&headers), PageMode::Full);
headers.insert(HEMX_PARTIAL_HEADER, "true".parse().unwrap());
assert_eq!(PageMode::from_headers(&headers), PageMode::Partial);
}
#[test]
fn page_request_wraps_full_pages_and_leaves_partials_unwrapped() {
// req: test/005
let full = PageRequest {
mode: PageMode::Full,
}
.page("<main data-page=\"docs\">Docs</main>", |content| {
format!("<html><body data-shell=\"docs\">{content}</body></html>")
});
assert_eq!(full.mode, PageMode::Full);
let full_document = Html::parse_document(&full.html);
assert_eq!(
full_document
.select(&selector(
"body[data-shell=\"docs\"] main[data-page=\"docs\"]"
))
.count(),
1
);
let partial = PageRequest {
mode: PageMode::Partial,
}
.page("<main data-page=\"docs\">Docs</main>", |content| {
format!("<html><body data-shell=\"docs\">{content}</body></html>")
});
assert_eq!(partial.mode, PageMode::Partial);
let partial_fragment = Html::parse_fragment(&partial.html);
assert_eq!(
partial_fragment
.select(&selector("main[data-page=\"docs\"]"))
.count(),
1
);
assert_eq!(
partial_fragment
.select(&selector("body[data-shell=\"docs\"]"))
.count(),
0
);
}
#[test]
fn page_request_wraps_safe_html_full_pages_and_leaves_partials_unwrapped() {
// req: axum_integration/001 req: html_safety/001 req: html_safety/002 req: test/005
let full = PageRequest {
mode: PageMode::Full,
}
.page_html(
SafeHtml::trusted("<main data-page=\"docs\">Docs</main>"),
|content| {
SafeHtml::trusted(format!(
"<html><body data-shell=\"docs\">{content}</body></html>"
))
},
);
assert_eq!(full.mode, PageMode::Full);
let full_document = Html::parse_document(&full.html);
assert_eq!(
full_document
.select(&selector(
"body[data-shell=\"docs\"] main[data-page=\"docs\"]"
))
.count(),
1
);
let partial = PageRequest {
mode: PageMode::Partial,
}
.page_html(
SafeHtml::trusted("<main data-page=\"docs\">Docs</main>"),
|content| {
SafeHtml::trusted(format!(
"<html><body data-shell=\"docs\">{content}</body></html>"
))
},
);
assert_eq!(partial.mode, PageMode::Partial);
let partial_fragment = Html::parse_fragment(&partial.html);
assert_eq!(
partial_fragment
.select(&selector("main[data-page=\"docs\"]"))
.count(),
1
);
assert_eq!(
partial_fragment
.select(&selector("body[data-shell=\"docs\"]"))
.count(),
0
);
}
#[test]
fn partial_page_response_sets_partial_and_title_headers() {
let response = PageResponse::partial("<main>Docs</main>")
.title("Docs")
.into_response();
assert_eq!(
response.headers()[header::CONTENT_TYPE],
"text/html; charset=utf-8"
);
assert_eq!(response.headers()[HEMX_PARTIAL_HEADER], "true");
assert_eq!(response.headers()[HEMX_TITLE_HEADER], "Docs");
}
#[test]
fn effect_response_is_wire_batch_with_fingerprint_header() {
let response = EffectResponse::new(push("/docs"), BuildFingerprint(99)).into_response();
assert_eq!(response.headers()[header::CONTENT_TYPE], HEMX_CONTENT_TYPE);
assert_eq!(response.headers()[HEMX_FINGERPRINT_HEADER], "99");
}
#[test]
fn interaction_form_parses_handle_and_fields() {
let form =
InteractionForm::parse_urlencoded(b"__h=42&title=Hello+World&tag=a&tag=b%2Fc").unwrap();
assert_eq!(form.handle_id, 42);
assert_eq!(form.value("title"), Some("Hello World"));
assert_eq!(form.values("tag").collect::<Vec<_>>(), ["a", "b/c"]);
}
#[test]
fn interaction_form_parses_typed_values() {
// req: form/004 req: dx/003
let form =
InteractionForm::parse_urlencoded(b"__h=42&count=7&bad=nope").expect("form should parse");
assert_eq!(form.parse::<u32>("count"), Some(7));
assert_eq!(form.parse::<u32>("bad"), None);
assert_eq!(form.parse::<u32>("missing"), None);
}
#[test]
fn interaction_form_requires_numeric_handle() {
assert_eq!(
InteractionForm::parse_urlencoded(b"title=Hello").unwrap_err(),
InteractionFormRejection::MissingHandle
);
assert_eq!(
InteractionForm::parse_urlencoded(b"__h=nope").unwrap_err(),
InteractionFormRejection::InvalidHandle
);
}
#[test]
fn interaction_form_preserves_hidden_csrf_fields_for_extractors() {
// req: auth/004
let form = InteractionForm::parse_urlencoded(b"__h=42&csrf_token=abc123&title=Hello").unwrap();
assert_eq!(form.handle_id, 42);
assert_eq!(form.value("csrf_token"), Some("abc123"));
assert!(form.fields().iter().any(|(name, _)| name == "csrf_token"));
}
#[test]
fn interaction_request_dispatches_with_concise_handlers_helper() {
// req: axum_integration/003 req: ceremony/001 req: dx/003
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(7),
Vec::new(),
));
let response = request
.dispatch(
interactions(BuildFingerprint(4))
.on(Handle::<()>::new(7), |_| Slot::<String>::new(3).text("ok")),
)
.unwrap();
assert_eq!(response.batch.fingerprint, BuildFingerprint(4));
assert_eq!(response.batch.ops, vec![Slot::<String>::new(3).text("ok")]);
}
#[test]
fn interaction_request_dispatches_typed_form_inputs() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/003
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(8),
vec![("project_id".to_owned(), "42".to_owned())],
));
let response = request
.dispatch(
interactions(BuildFingerprint(4))
.on_form(Handle::<()>::new(8), |input: OpenProject| {
Slot::<String>::new(3).text(input.project_id.0)
}),
)
.unwrap();
assert_eq!(response.batch.fingerprint, BuildFingerprint(4));
assert_eq!(response.batch.ops, vec![Slot::<String>::new(3).text(42)]);
}
#[test]
fn interaction_request_rejects_invalid_typed_form_inputs() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/004
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(8),
vec![("project_id".to_owned(), "nope".to_owned())],
));
let rejection = request
.dispatch(
interactions(BuildFingerprint(4))
.on_form(Handle::<()>::new(8), |input: OpenProject| {
Slot::<String>::new(3).text(input.project_id.0)
}),
)
.unwrap_err();
assert!(matches!(
rejection,
DispatchRejection::InvalidForm { handle_id: 8, .. }
));
}
#[test]
fn interaction_request_dispatches_typed_state_handlers() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/003
fn open_project(multiplier: u32, input: OpenProject) -> impl IntoEffect {
Slot::<String>::new(3).text(input.project_id.0 * multiplier)
}
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(9),
vec![("project_id".to_owned(), "21".to_owned())],
));
let response = request
.dispatch(
interactions(BuildFingerprint(4))
.with_state(2_u32)
.on(Handle::<()>::new(9), open_project),
)
.unwrap();
assert_eq!(response.batch.ops, vec![Slot::<String>::new(3).text(42)]);
}
#[derive(Debug)]
struct HandlerBoom;
impl std::fmt::Display for HandlerBoom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("database unavailable")
}
}
impl std::error::Error for HandlerBoom {}
#[tokio::test]
async fn interaction_request_dispatches_async_typed_state_extractors() {
// req: axum_integration/003 req: form/004 req: canonical_authoring/003
async fn open_project(
State(multiplier): State<u32>,
Form(input): Form<OpenProject>,
) -> impl IntoEffect {
Slot::<String>::new(3).text(input.project_id.0 * multiplier)
}
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(10),
vec![("project_id".to_owned(), "21".to_owned())],
));
let response = request
.dispatch_async(
interactions(BuildFingerprint(4))
.with_state(2_u32)
.on_async(Handle::<()>::new(10), open_project)
.into_registry(),
)
.await
.unwrap();
assert_eq!(response.batch.ops, vec![Slot::<String>::new(3).text(42)]);
}
#[tokio::test]
async fn interaction_request_reports_async_result_handler_errors() {
// req: axum_integration/003 req: form/004 req: failure/003
async fn open_project(
State(_multiplier): State<u32>,
Form(_input): Form<OpenProject>,
) -> Result<impl IntoEffect, HandlerBoom> {
Err::<(), _>(HandlerBoom)
}
let request = InteractionRequest::from(InteractionForm::for_handle(
Handle::<()>::new(11),
vec![("project_id".to_owned(), "21".to_owned())],
));
let error = request
.dispatch_async(
interactions(BuildFingerprint(4))
.with_state(2_u32)
.on_async_result(Handle::<()>::new(11), open_project)
.into_registry(),
)
.await
.unwrap_err();
assert_eq!(
error,
DispatchRejection::HandlerError {
handle_id: 11,
message: "database unavailable".to_owned(),
}
);
}
#[test]
fn interactions_dispatch_by_checked_handle() {
// req: ceremony/004 req: public_api/001
let title = Slot::<String>::new(7);
let create = Handle::<()>::new(42);
let request = InteractionRequest::from(InteractionForm::for_handle(
create,
[("title".into(), "Hello".into())],
));
let response = request
.dispatch(interactions(BuildFingerprint(123)).on(create, move |form| {
title.text(form.value("title").unwrap_or(""))
}))
.unwrap();
assert_eq!(response.batch.fingerprint, BuildFingerprint(123));
assert_eq!(response.batch.ops, vec![title.text("Hello")]);
}
#[test]
fn interactions_reject_unknown_handle_ids() {
let request = InteractionRequest::from(InteractionForm::new(9, []));
assert_eq!(
request
.dispatch(interactions(BuildFingerprint(123)))
.unwrap_err(),
DispatchRejection::UnknownHandle(9)
);
}
#[test]
fn runtime_js_response_serves_embedded_runtime() {
let response = runtime_js().into_response();
assert_eq!(
response.headers()[header::CONTENT_TYPE],
HEMX_RUNTIME_CONTENT_TYPE
);
assert!(response.headers().contains_key(header::CACHE_CONTROL));
}