3131//! here — behaviour flows producer → OGAR `ActionDef` → Rust method.
3232
3333use askama:: Template ;
34- use lance_graph_contract:: class_view:: FieldMask ;
34+ use lance_graph_contract:: class_view:: { FieldMask , WideFieldMask } ;
3535use ogar_vocab:: { canonical_concept_id, ActionDef , AssociationKind , Class } ;
3636
3737use 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