Skip to content

Commit a81fc4e

Browse files
authored
Merge pull request #163 from AdaWorldAPI/claude/wide-mask-render
render-askama: render_class_with_methods_wide — the WideFieldMask entry
2 parents 0dad0c3 + 101e2b8 commit a81fc4e

2 files changed

Lines changed: 167 additions & 5 deletions

File tree

crates/ogar-render-askama/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ pub use artifact_kinds::{
8181
};
8282
pub use form_view::{default_input_kind_for, InputKind};
8383
pub use list_view::{default_kind_for, ColumnKind, RenderColumn, SortOrder};
84-
pub use rust_class::{render_class_with_methods, RenderError};
84+
pub use rust_class::{render_class_with_methods, render_class_with_methods_wide, RenderError};
8585
pub use spec::{ArtifactKind, ArtifactSpec};
8686

8787
use ogar_vocab::Class;

crates/ogar-render-askama/src/rust_class.rs

Lines changed: 166 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
//! here — behaviour flows producer → OGAR `ActionDef` → Rust method.
3232
3333
use askama::Template;
34-
use lance_graph_contract::class_view::FieldMask;
34+
use lance_graph_contract::class_view::{FieldMask, WideFieldMask};
3535
use ogar_vocab::{canonical_concept_id, ActionDef, AssociationKind, Class};
3636

3737
use crate::artifact_kinds::rust_struct::{edge_rust_type, escape_rust_ident, rails_to_rust_type};
@@ -148,17 +148,61 @@ pub fn render_class_with_methods(
148148
});
149149
}
150150

151+
render_class_with_methods_via(class, |idx| field_present(mask, idx), actions)
152+
}
153+
154+
/// Render a canonical [`Class`] as a Rust struct, gated by a [`WideFieldMask`]
155+
/// instead of the single-`u64` [`FieldMask`] — the additive entry point for
156+
/// classes whose field count may exceed [`FieldMask::MAX_FIELDS`] (64).
157+
///
158+
/// Unlike [`render_class_with_methods`], there is no `TooManyFieldsForMask`
159+
/// loud-fail here: `WideFieldMask` addresses every field position natively
160+
/// (promoting `Small` → `Wide` past 64 on demand — see [`WideFieldMask::with`]),
161+
/// so there is no ceiling to guard against. The caller passes
162+
/// [`WideFieldMask::full_for`]`(field_count)` for the "unmasked, emit
163+
/// everything" sentinel — the wide-tier sibling of [`FieldMask::FULL`].
164+
///
165+
/// Same projection, same template, same DO-arm lifting as
166+
/// [`render_class_with_methods`] — the two entry points share
167+
/// [`render_class_with_methods_via`] and differ only in which mask type
168+
/// answers presence.
169+
///
170+
/// # Errors
171+
///
172+
/// Propagates [`RenderError::Askama`] if template rendering fails (it never
173+
/// should for well-formed input — the template has no fallible expressions).
174+
pub fn render_class_with_methods_wide(
175+
class: &Class,
176+
mask: &WideFieldMask,
177+
actions: &[ActionDef],
178+
) -> Result<String, RenderError> {
179+
render_class_with_methods_via(class, |idx| mask.has(idx), actions)
180+
}
181+
182+
/// Shared ctx-build core for both [`render_class_with_methods`] (narrow,
183+
/// [`FieldMask`]) and [`render_class_with_methods_wide`] (wide,
184+
/// [`WideFieldMask`]). `field_present` answers "is field position `idx`
185+
/// populated" — the only axis the two public entry points differ on; the
186+
/// projection, constructor synthesis, DO-arm lifting, and template render are
187+
/// identical for both. The ceiling guard (`TooManyFieldsForMask`) is NOT
188+
/// here — it is [`render_class_with_methods`]'s narrow-mask-only concern, so
189+
/// this helper never rejects a wide class.
190+
fn render_class_with_methods_via(
191+
class: &Class,
192+
field_present: impl Fn(u8) -> bool,
193+
actions: &[ActionDef],
194+
) -> Result<String, RenderError> {
151195
let concept = class.canonical_concept.as_deref().unwrap_or("");
152196
let class_id_hex = canonical_concept_id(concept)
153197
.map(|id| format!("0x{id:04X}"))
154198
.unwrap_or_default();
155199

156200
// ObjectView N3 order: attributes, then associations. `idx` walks the
157-
// combined sequence; the mask gates each position.
201+
// combined sequence; `field_present` gates each position.
158202
let mut fields = Vec::new();
159203
let mut idx: u8 = 0;
160204
for a in &class.attributes {
161-
if field_present(mask, idx) {
205+
if field_present(idx) {
162206
fields.push(RustField {
163207
snake_name: escape_rust_ident(&a.name),
164208
rust_type: rails_to_rust_type(a.type_name.as_deref()),
@@ -168,7 +212,7 @@ pub fn render_class_with_methods(
168212
idx = idx.saturating_add(1);
169213
}
170214
for e in &class.associations {
171-
if field_present(mask, idx) {
215+
if field_present(idx) {
172216
fields.push(RustField {
173217
snake_name: escape_rust_ident(&e.name),
174218
rust_type: edge_rust_type(e),
@@ -500,6 +544,124 @@ mod tests {
500544
assert!(render_class_with_methods(&small, FieldMask::EMPTY.with(0), &[]).is_ok());
501545
}
502546

547+
/// Minimal replica of `tests/mask_dual_target.rs::present_field_names` —
548+
/// test-local there (a separate integration-test binary), so it is not
549+
/// reachable from this unit-test module; re-implemented here rather than
550+
/// imported.
551+
fn present_field_names(src: &str) -> std::collections::BTreeSet<String> {
552+
src.lines()
553+
.filter_map(|line| {
554+
let trimmed = line.trim();
555+
let rest = trimmed.strip_prefix("pub ")?;
556+
if !trimmed.ends_with(',') || rest.starts_with("const ") || rest.starts_with("fn ")
557+
{
558+
return None;
559+
}
560+
let ident = rest.split(':').next()?;
561+
Some(ident.trim().to_string())
562+
})
563+
.collect()
564+
}
565+
566+
/// (a) A wide class (70 fields, one past the 64-bit `FieldMask` ceiling)
567+
/// under a partial `WideFieldMask` emits EXACTLY the masked fields —
568+
/// including a position past the ceiling (`f65`), which `FieldMask`
569+
/// cannot represent at all.
570+
#[test]
571+
fn wide_mask_gates_which_fields_emit_on_a_wide_class() {
572+
let class = wide_class();
573+
let mask = WideFieldMask::from_positions(&[0, 65]);
574+
let src = render_class_with_methods_wide(&class, &mask, &[]).unwrap();
575+
576+
let present = present_field_names(&src);
577+
let expected: std::collections::BTreeSet<String> =
578+
["f0".to_string(), "f65".to_string()].into_iter().collect();
579+
assert_eq!(
580+
present, expected,
581+
"only the masked positions (f0, f65) should be present:\n{src}"
582+
);
583+
// Spot-check a few explicitly absent positions.
584+
assert!(!src.contains("pub f1:"), "{src}");
585+
assert!(!src.contains("pub f64:"), "{src}");
586+
assert!(!src.contains("pub f69:"), "{src}");
587+
}
588+
589+
/// (b) A wide class under `WideFieldMask::full_for(field_count)` emits
590+
/// every field, including every position past the 64-bit ceiling.
591+
#[test]
592+
fn wide_mask_full_for_emits_all_fields_on_a_wide_class() {
593+
let class = wide_class();
594+
let mask = WideFieldMask::full_for(class.attributes.len());
595+
let src = render_class_with_methods_wide(&class, &mask, &[]).unwrap();
596+
597+
let present = present_field_names(&src);
598+
assert_eq!(present.len(), 70, "all 70 fields must be present:\n{src}");
599+
for i in 0..70 {
600+
assert!(
601+
src.contains(&format!("pub f{i}:")),
602+
"field f{i} missing under full_for:\n{src}"
603+
);
604+
}
605+
}
606+
607+
/// (c) For a narrow (<=64-field) class, the wide entry point must emit
608+
/// BYTE-IDENTICAL output to the narrow entry point, both for the "emit
609+
/// everything" sentinel and for a partial mask — proving the shared
610+
/// `render_class_with_methods_via` refactor changed no behaviour.
611+
#[test]
612+
fn wide_entry_matches_narrow_entry_byte_identical_for_narrow_class() {
613+
let class = sample_class();
614+
let actions = sample_actions();
615+
616+
// "Emit everything" sentinel, both sides.
617+
let narrow_full = render_class_with_methods(&class, FieldMask::FULL, &actions).unwrap();
618+
let wide_full = render_class_with_methods_wide(
619+
&class,
620+
&WideFieldMask::full_for(class.attributes.len()),
621+
&actions,
622+
)
623+
.unwrap();
624+
assert_eq!(
625+
narrow_full, wide_full,
626+
"FULL narrow vs full_for wide must render byte-identical output"
627+
);
628+
629+
// A partial mask (only field 0 present), both sides.
630+
let narrow_partial =
631+
render_class_with_methods(&class, FieldMask::EMPTY.with(0), &[]).unwrap();
632+
let wide_partial =
633+
render_class_with_methods_wide(&class, &WideFieldMask::from_positions(&[0]), &[])
634+
.unwrap();
635+
assert_eq!(
636+
narrow_partial, wide_partial,
637+
"partial narrow mask vs equivalent wide mask must render byte-identical output"
638+
);
639+
}
640+
641+
/// (d) Regression: the narrow entry's >64 loud-fail guard is untouched by
642+
/// this refactor — a wide class under a partial `FieldMask` still errors
643+
/// via `render_class_with_methods` — while the wide entry, which has no
644+
/// such ceiling, succeeds on the very same class under the equivalent
645+
/// partial `WideFieldMask`.
646+
#[test]
647+
fn narrow_entry_still_loud_fails_over_64_while_wide_entry_succeeds() {
648+
let class = wide_class();
649+
650+
let err = render_class_with_methods(&class, FieldMask::EMPTY.with(0), &[]).unwrap_err();
651+
assert!(
652+
matches!(err, RenderError::TooManyFieldsForMask { field_count: 70, max } if max == FieldMask::MAX_FIELDS),
653+
"narrow entry must still loud-fail on a wide class under a partial mask: {err:?}"
654+
);
655+
656+
// The wide entry has no ceiling to guard: the same class, an
657+
// equivalent partial mask, succeeds.
658+
assert!(
659+
render_class_with_methods_wide(&class, &WideFieldMask::from_positions(&[0]), &[])
660+
.is_ok(),
661+
"wide entry must not loud-fail — WideFieldMask has no 64-field ceiling"
662+
);
663+
}
664+
503665
#[test]
504666
fn sanitize_ident_handles_dotted_and_leading_digit() {
505667
assert_eq!(sanitize_ident("action_post"), "action_post");

0 commit comments

Comments
 (0)