From 03a871bc1aeefd96f0d634c58d6e786501913c7b Mon Sep 17 00:00:00 2001 From: Satya Date: Fri, 3 Jul 2026 20:04:16 -0400 Subject: [PATCH] filter synthesis: named value types get their own synthesized, referenced filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes the inline-structural approach (0d282cc): inlining erased the value type's name, so identical nested shapes (weekly_hours at two paths) generated duplicate leaf types downstream and Dart barrel exports collided. Now a named non-table value type's filter is synthesized ONCE (like table-backed boundaries) and property references point at it — mirroring how the entity side generates one class per named type. Same filter-by-fields capability; laziness also removes the structural-recursion hazard. A named type with no compilable structure still gets no filter and is omitted rather than dangled. Co-Authored-By: Claude Fable 5 --- fixtures/filter.json | 89 ++++++++++++++++++++++------------ src/database/compile/filter.rs | 49 ++++--------------- src/database/mod.rs | 28 +++++++++-- 3 files changed, 91 insertions(+), 75 deletions(-) diff --git a/fixtures/filter.json b/fixtures/filter.json index 4c468f8..745f703 100644 --- a/fixtures/filter.json +++ b/fixtures/filter.json @@ -400,38 +400,9 @@ }, "schedule": { "type": [ - "filter", + "opening_hours.filter", "null" - ], - "compiledPropertyNames": [ - "open", - "seasons" - ], - "properties": { - "open": { - "type": [ - "string.condition", - "null" - ] - }, - "seasons": { - "type": [ - "filter", - "null" - ], - "compiledPropertyNames": [ - "label" - ], - "properties": { - "label": { - "type": [ - "string.condition", - "null" - ] - } - } - } - } + ] } }, "type": "filter" @@ -551,7 +522,61 @@ } }, "opening_hours": {}, - "season": {} + "season": {}, + "opening_hours.filter": { + "type": "filter", + "compiledPropertyNames": [ + "open", + "seasons", + "$and", + "$or" + ], + "properties": { + "open": { + "type": [ + "string.condition", + "null" + ] + }, + "seasons": { + "type": [ + "season.filter", + "null" + ] + }, + "$and": { + "type": [ + "array", + "null" + ], + "items": { + "type": "opening_hours.filter", + "compiledPropertyNames": [ + "open", + "seasons", + "$and", + "$or" + ] + } + }, + "$or": { + "type": [ + "array", + "null" + ], + "items": { + "type": "opening_hours.filter", + "compiledPropertyNames": [ + "open", + "seasons", + "$and", + "$or" + ] + } + } + } + }, + "season.filter": {} } } } diff --git a/src/database/compile/filter.rs b/src/database/compile/filter.rs index 783df37..0a97c8d 100644 --- a/src/database/compile/filter.rs +++ b/src/database/compile/filter.rs @@ -26,14 +26,10 @@ impl Schema { if let Some(items) = &child.obj.items { if !items.is_proxy() { structural_filter = items.compile_filter(_db, "", _errors); - } else if let Some(target) = Self::value_type_target(items, _db) { - structural_filter = target.compile_filter(_db, "", _errors); } } } else if !child.is_proxy() { structural_filter = child.compile_filter(_db, "", _errors); - } else if let Some(target) = Self::value_type_target(child, _db) { - structural_filter = target.compile_filter(_db, "", _errors); } if let Some(mut inline_schema) = structural_filter { @@ -121,37 +117,6 @@ impl Schema { None } - /// Resolves a pure type-pointer schema to a named non-table value type's own schema — - /// a reusable, schema-only object (e.g. an operating-hours config). Naming a value type - /// is a reuse choice, not a semantics choice: it filters structurally, exactly like an - /// inline object. Table-backed boundaries keep the lazy {type}.filter reference instead. - fn value_type_target<'a>(schema: &Arc, db: &'a Database) -> Option<&'a Arc> { - let t = match &schema.obj.type_ { - Some(SchemaTypeOrArray::Single(t)) => Some(t.as_str()), - Some(SchemaTypeOrArray::Multiple(types)) => { - types.iter().find(|t| *t != "null").map(|s| s.as_str()) - } - None => None, - }?; - if matches!( - t, - "string" | "integer" | "number" | "boolean" | "object" | "array" | "null" - ) { - return None; - } - if t.ends_with(".condition") || t.ends_with(".filter") { - return None; - } - if db.enums.contains_key(t) { - return None; - } - let base = t.split('.').next_back().unwrap_or(t); - if db.types.contains_key(base) { - return None; - } - db.schemas.get(t) - } - fn resolve_filter_type(schema: &Arc, db: &Database) -> Option> { if let Some(type_) = &schema.obj.type_ { match type_ { @@ -200,12 +165,16 @@ impl Schema { } else if db.enums.contains_key(custom) { Some(vec![format!("{}.condition", custom)]) } else { - // A Relational cross-boundary gets a reference to the target's dynamically built - // .filter — but only a table-backed boundary has one. A named non-table value type - // compiles structurally upstream (value_type_target); reaching here means it had - // no compilable structure — omit it rather than emit a dangling .filter reference. + // A named type gets a reference to its dynamically built .filter — either a + // Relational cross-boundary (table-backed) or a named value type, whose filter + // is likewise synthesized (see Database::compile_filters). A named type with no + // compilable structure gets no filter — omit it rather than dangle a reference. let base = custom.split('.').next_back().unwrap_or(custom); - if db.types.contains_key(base) { + let has_value_filter = db + .schemas + .get(custom) + .map_or(false, |s| Database::is_value_filter_candidate(custom, s)); + if db.types.contains_key(base) || has_value_filter { Some(vec![format!("{}.filter", custom)]) } else { None diff --git a/src/database/mod.rs b/src/database/mod.rs index 1934cf4..2798e9b 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -262,15 +262,23 @@ impl Database { } } - /// Synthesizes Composed Filter References for all table-backed boundaries. + /// Synthesizes Composed Filter References for all table-backed boundaries — and for + /// named non-table value types (schema-only objects, e.g. an operating-hours config), + /// so a property reference resolves to ONE named filter instead of inlining anonymous + /// per-path copies (which duplicate identical leaf type names for downstream codegen). fn compile_filters(&mut self, errors: &mut Vec) -> Vec<(String, String)> { let mut filter_schemas = Vec::new(); + let mut seen_value_ids = std::collections::HashSet::new(); for (type_name, type_def) in &self.types { for (id, schema_arc) in &type_def.schemas { - // Only run synthesis on actual structured, table-backed boundaries. Exclude subschemas! + // Run synthesis on structured table-backed boundaries and named value types. + // Exclude subschemas! let base_name = id.split('.').last().unwrap_or(id); let is_table_backed = base_name == type_def.name; - if is_table_backed && !id.contains('/') { + let is_value_type = !is_table_backed + && Self::is_value_filter_candidate(id, schema_arc) + && seen_value_ids.insert(id.clone()); + if (is_table_backed || is_value_type) && !id.contains('/') { if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) { filter_schemas.push(( type_name.clone(), @@ -293,6 +301,20 @@ impl Database { filter_ids } + /// A named non-table value type that earns its own synthesized filter: a bare-named + /// (dotless) schema-only object with compiled properties. The base `filter`/`condition` + /// schemas are infrastructure, not value types. + pub fn is_value_filter_candidate(id: &str, schema: &Arc) -> bool { + !id.contains('.') + && id != "filter" + && id != "condition" + && schema + .obj + .compiled_properties + .get() + .map_or(false, |props| !props.is_empty()) + } + /// Synthesizes strong Enum Conditions mirroring the string.condition capabilities. fn compile_conditions(&mut self) -> Vec<(String, String)> { let mut enum_conditions = Vec::new();