- ssr double: page template composing header/footer partials under the
HTML escaper, typed context built by a builder fn per ADR-004; golden
pins for composition, escaping of hostile interpolation, the trusted
{{& }} slot, and render-by-partial-name rejection
- code double: struct-writer template under the no-op escaper, typed
context built from a schema document with kind mapping and
source-escaped literals in the builder; golden pins for document-order
arrays and the active preserve_order feature
- fixture assets are include_str! files under tests/fixtures/; fixture
modules are per-test-binary so wave-2 proofs can grow them
- task file updated to completed with Notes and Summary
Verification: cargo test green (19 lib + 8 fixture tests), clippy
-D warnings clean, cargo fmt --check clean
253 lines
8.0 KiB
Rust
253 lines
8.0 KiB
Rust
//! The code-shaped engine fixture double (posture-A/C shape): a
|
|
//! struct-writer template over a typed context built from a schema
|
|
//! document by a Rust builder, with a no-op escaper and
|
|
//! document-ordered arrays, pinned by golden renders.
|
|
|
|
use alkgen::{EscapePolicy, TemplateSet};
|
|
use serde::Serialize;
|
|
|
|
mod fixtures {
|
|
pub const STRUCT_WRITER: &str = include_str!("fixtures/code/struct-writer.hbs");
|
|
pub const SCHEMA: &str = include_str!("fixtures/code/schema.json");
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct StructWriterContext {
|
|
struct_name: String,
|
|
fields: Vec<WriterField>,
|
|
default_consts: Vec<DefaultConst>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct WriterField {
|
|
name: String,
|
|
writer_call: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct DefaultConst {
|
|
const_name: String,
|
|
literal: String,
|
|
}
|
|
|
|
/// The schema-kind to writer-fn mapping, one of the ADR-004 kind
|
|
/// mappings: the template renders whatever call name the builder
|
|
/// decided and never interprets a schema kind itself.
|
|
fn writer_call(schema_kind: &str) -> String {
|
|
match schema_kind {
|
|
"u32" => "write_u32".to_owned(),
|
|
"u64" => "write_u64".to_owned(),
|
|
"string" => "write_string".to_owned(),
|
|
"bool" => "write_bool".to_owned(),
|
|
other => panic!("fixture schema must only carry known kinds, found: {other}"),
|
|
}
|
|
}
|
|
|
|
/// The Rust-type-name mapping (snake_case document name to
|
|
/// PascalCase type): another ADR-004 decision living in builder code.
|
|
fn rust_struct_name(doc_name: &str) -> String {
|
|
doc_name
|
|
.split('_')
|
|
.map(|part| {
|
|
let mut chars = part.chars();
|
|
match chars.next() {
|
|
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
|
None => String::new(),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Rust source-level string-literal quoting: the codegen posture
|
|
/// escapes literals at the source (the builder), never by entity
|
|
/// mangling, which is why the code set renders with the no-op escaper.
|
|
fn rust_string_literal(raw: &str) -> String {
|
|
let mut literal = String::new();
|
|
literal.push('"');
|
|
for c in raw.chars() {
|
|
match c {
|
|
'"' => literal.push_str("\\\""),
|
|
'\\' => literal.push_str("\\\\"),
|
|
_ => literal.push(c),
|
|
}
|
|
}
|
|
literal.push('"');
|
|
literal
|
|
}
|
|
|
|
/// The ADR-004 context builder over the schema document: fields stay
|
|
/// in the document's array order (convention 5), string defaults are
|
|
/// builder-quoted into a `const`, and the struct name is mapped to the
|
|
/// Rust spelling, all in Rust.
|
|
fn build_struct_writer(doc: &serde_json::Value) -> StructWriterContext {
|
|
let struct_name = doc
|
|
.get("name")
|
|
.and_then(serde_json::Value::as_str)
|
|
.expect("fixture schema should carry a struct name");
|
|
let fields_doc = doc
|
|
.get("fields")
|
|
.and_then(serde_json::Value::as_array)
|
|
.expect("fixture schema should carry a fields array");
|
|
|
|
let mut fields = Vec::new();
|
|
let mut default_consts = Vec::new();
|
|
for field in fields_doc {
|
|
let name = field
|
|
.get("name")
|
|
.and_then(serde_json::Value::as_str)
|
|
.expect("fixture schema field should carry a name");
|
|
let kind = field
|
|
.get("kind")
|
|
.and_then(serde_json::Value::as_str)
|
|
.expect("fixture schema field should carry a kind");
|
|
fields.push(WriterField {
|
|
name: name.to_owned(),
|
|
writer_call: writer_call(kind),
|
|
});
|
|
if let Some(default) = field.get("default").and_then(serde_json::Value::as_str) {
|
|
default_consts.push(DefaultConst {
|
|
const_name: format!("{}_DEFAULT", name.to_uppercase()),
|
|
literal: rust_string_literal(default),
|
|
});
|
|
}
|
|
}
|
|
|
|
StructWriterContext {
|
|
struct_name: rust_struct_name(struct_name),
|
|
fields,
|
|
default_consts,
|
|
}
|
|
}
|
|
|
|
/// The code-shaped fixture set: one `struct-writer` template kind
|
|
/// under the no-op escaper.
|
|
fn code_fixture_set() -> TemplateSet {
|
|
let mut set = TemplateSet::new(EscapePolicy::NoOp);
|
|
set.register_template("struct-writer", fixtures::STRUCT_WRITER)
|
|
.expect("fixture struct-writer template should be well-formed");
|
|
set
|
|
}
|
|
|
|
fn golden_context() -> StructWriterContext {
|
|
let doc: serde_json::Value =
|
|
serde_json::from_str(fixtures::SCHEMA).expect("fixture schema should parse as JSON");
|
|
build_struct_writer(&doc)
|
|
}
|
|
|
|
const GOLDEN_STRUCT_WRITER: &str = concat!(
|
|
"pub const LABEL_DEFAULT: &str = \"Say \\\"<b>&\\\" hi\";\n",
|
|
"impl SampleRecord {\n",
|
|
" pub fn write<W: std::io::Write>(&self, out: &mut W) -> std::io::Result<()> {\n",
|
|
" out.write_u32(&self.magic)?;\n",
|
|
" out.write_u64(&self.length)?;\n",
|
|
" out.write_string(&self.label)?;\n",
|
|
" out.write_bool(&self.active)?;\n",
|
|
" Ok(())\n",
|
|
" }\n",
|
|
"}\n",
|
|
);
|
|
|
|
#[test]
|
|
fn struct_writer_golden_render_pins_kind_mapping_and_literals() {
|
|
let set = code_fixture_set();
|
|
let rendered = set
|
|
.render("struct-writer", &golden_context())
|
|
.expect("the golden struct-writer should render");
|
|
assert_eq!(rendered, GOLDEN_STRUCT_WRITER);
|
|
}
|
|
|
|
#[test]
|
|
fn noop_escaping_passes_source_escaped_literals_verbatim() {
|
|
let set = code_fixture_set();
|
|
let rendered = set
|
|
.render("struct-writer", &golden_context())
|
|
.expect("the struct-writer should render");
|
|
|
|
assert!(
|
|
rendered.contains(r#""Say \"<b>&\" hi""#),
|
|
"the builder-quoted literal must pass through the no-op escaper verbatim"
|
|
);
|
|
assert!(
|
|
!rendered.contains("<"),
|
|
"the HTML escaper would mangle the literal; the code set must never entity-escape"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn document_order_arrays_render_in_insertion_order() {
|
|
let set = code_fixture_set();
|
|
let rendered = set
|
|
.render("struct-writer", &golden_context())
|
|
.expect("the struct-writer should render");
|
|
|
|
let magic = rendered
|
|
.find("write_u32")
|
|
.expect("magic line should render");
|
|
let length = rendered
|
|
.find("write_u64")
|
|
.expect("length line should render");
|
|
let label = rendered
|
|
.find("write_string")
|
|
.expect("label line should render");
|
|
let active = rendered
|
|
.find("write_bool")
|
|
.expect("active line should render");
|
|
|
|
assert!(magic < length);
|
|
assert!(length < label);
|
|
assert!(label < active);
|
|
}
|
|
|
|
#[test]
|
|
fn insertion_order_drives_render_order_not_sorting() {
|
|
let set = code_fixture_set();
|
|
let mut doc: serde_json::Value =
|
|
serde_json::from_str(fixtures::SCHEMA).expect("fixture schema should parse as JSON");
|
|
let fields = doc
|
|
.get_mut("fields")
|
|
.and_then(serde_json::Value::as_array_mut)
|
|
.expect("fixture schema should carry a fields array");
|
|
fields.sort_by_key(|field| {
|
|
field
|
|
.get("name")
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(str::to_owned)
|
|
.unwrap_or_default()
|
|
});
|
|
|
|
let rendered = set
|
|
.render("struct-writer", &build_struct_writer(&doc))
|
|
.expect("the reordered struct-writer should render");
|
|
|
|
let active = rendered
|
|
.find("write_bool")
|
|
.expect("active line should render");
|
|
let label = rendered
|
|
.find("write_string")
|
|
.expect("label line should render");
|
|
let length = rendered
|
|
.find("write_u64")
|
|
.expect("length line should render");
|
|
let magic = rendered
|
|
.find("write_u32")
|
|
.expect("magic line should render");
|
|
|
|
assert!(active < label);
|
|
assert!(label < length);
|
|
assert!(length < magic);
|
|
}
|
|
|
|
#[test]
|
|
fn serde_json_preserve_order_is_active_in_the_dep_set() {
|
|
let mut map = serde_json::Map::new();
|
|
map.insert("beta".to_owned(), serde_json::json!(2));
|
|
map.insert("alpha".to_owned(), serde_json::json!(1));
|
|
|
|
let value = serde_json::Value::Object(map);
|
|
assert_eq!(
|
|
value.to_string(),
|
|
r#"{"beta":2,"alpha":1}"#,
|
|
"object insertion order must survive serialization; disabling the preserve_order feature sorts keys and fails this pin"
|
|
);
|
|
}
|