//! 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, default_consts: Vec, } #[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::() + 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 \\\"&\\\" hi\";\n", "impl SampleRecord {\n", " pub fn 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 \"&\" 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" ); }