Class: Synthra::Generator::Resolver
- Inherits:
-
Object
- Object
- Synthra::Generator::Resolver
- Defined in:
- lib/synthra/generator/resolver.rb
Overview
Resolves cross-schema references
Instance Method Summary collapse
-
#build_dependency_graph ⇒ Hash
private
Build dependency graph from schema references.
-
#clear_cache ⇒ void
Clear the resolution cache.
-
#dependency_order ⇒ Array<String>
Get schemas in dependency order.
-
#detect_cycles ⇒ Array<Array<String>>
Detect cycles in schema references.
-
#detect_cycles_from(node, visited, stack, cycles) ⇒ void
private
Detect cycles starting from a node.
-
#extract_path(record, path) ⇒ Object?
private
Extract value from record using dot-separated path.
-
#initialize(registry) ⇒ Resolver
constructor
A new instance of Resolver.
-
#resolve(schema_name, field_path, depth: 0, seed: nil) ⇒ Object
Resolve a reference to another schema's field.
-
#topological_sort(graph) ⇒ Array<String>
private
Topologically sort schemas by dependencies.
Constructor Details
#initialize(registry) ⇒ Resolver
Returns a new instance of Resolver.
8 9 10 11 12 13 |
# File 'lib/synthra/generator/resolver.rb', line 8 def initialize(registry) @registry = registry @cache = {} @generating = Set.new @mutex = Mutex.new end |
Instance Method Details
#build_dependency_graph ⇒ Hash (private)
Build dependency graph from schema references
Analyzes all schemas to build a graph of cross-schema dependencies.
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/synthra/generator/resolver.rb', line 160 def build_dependency_graph graph = Hash.new { |h, k| h[k] = [] } @registry.each do |name, schema| schema.fields.each do |field| if field.type_name == "reference" ref = field.type_args[:ref] ref_schema = ref.respond_to?(:schema_name) ? ref.schema_name : ref&.dig(:schema_name) graph[name] << ref_schema if ref_schema end end end graph end |
#clear_cache ⇒ void
This method returns an undefined value.
Clear the resolution cache
116 117 118 119 120 121 |
# File 'lib/synthra/generator/resolver.rb', line 116 def clear_cache @mutex.synchronize do @cache.clear @generating.clear end end |
#dependency_order ⇒ Array<String>
Get schemas in dependency order
89 90 91 92 |
# File 'lib/synthra/generator/resolver.rb', line 89 def dependency_order dependencies = build_dependency_graph topological_sort(dependencies) end |
#detect_cycles ⇒ Array<Array<String>>
Detect cycles in schema references
99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/synthra/generator/resolver.rb', line 99 def detect_cycles cycles = [] visited = Set.new stack = [] @registry.names.each do |name| detect_cycles_from(name, visited, stack, cycles) end cycles end |
#detect_cycles_from(node, visited, stack, cycles) ⇒ void (private)
This method returns an undefined value.
Detect cycles starting from a node
Recursively traverses the dependency graph to detect cycles. Uses depth-first search with cycle detection.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/synthra/generator/resolver.rb', line 225 def detect_cycles_from(node, visited, stack, cycles) return if visited.include?(node) if stack.include?(node) cycle_start = stack.index(node) cycles << stack[cycle_start..] + [node] return end stack.push(node) begin schema = @registry.schema(node) schema.fields.each do |field| if field.type_name == "reference" ref = field.type_args[:ref] ref_schema = ref.respond_to?(:schema_name) ? ref.schema_name : ref&.dig(:schema_name) detect_cycles_from(ref_schema, visited, stack, cycles) if ref_schema end end rescue KeyError # Schema not found end stack.pop visited.add(node) end |
#extract_path(record, path) ⇒ Object? (private)
Extract value from record using dot-separated path
Traverses nested hashes using dot notation (e.g., "address.city").
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/synthra/generator/resolver.rb', line 135 def extract_path(record, path) parts = path.to_s.split(".") value = record parts.each do |part| case value when Hash value = value[part] || value[part.to_sym] else return nil end return nil if value.nil? end value end |
#resolve(schema_name, field_path, depth: 0, seed: nil) ⇒ Object
Resolve a reference to another schema's field
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/synthra/generator/resolver.rb', line 26 def resolve(schema_name, field_path, depth: 0, seed: nil) # Validate recursion depth against configured limits (before mutex to avoid deadlock) # Check depth + 1 because we're about to generate at that depth Synthra.configuration.limits.validate_recursion!(depth + 1) # Check cache first (thread-safe read) cache_key = "#{schema_name}.#{field_path}" @mutex.synchronize do return @cache[cache_key] if @cache.key?(cache_key) end # Check for cycles BEFORE acquiring mutex to avoid deadlock on recursive calls @mutex.synchronize do if @generating.include?(schema_name) cycle = @generating.to_a + [schema_name] raise CyclicReferenceError, cycle end end # Generate the referenced schema (outside mutex to allow recursive calls) begin @mutex.synchronize do @generating.add(schema_name) end schema = @registry.schema(schema_name) # Pass this resolver instance to ensure cycle detection works # The Engine will reuse this resolver instead of creating a new one. # `seed` (from the parent RNG) makes the referenced record reproducible by seed. record = schema.generate(seed: seed, registry: @registry, depth: depth + 1, resolver: self) # Cache all fields from this schema @mutex.synchronize do record.each do |key, val| @cache["#{schema_name}.#{key}"] = val end # Extract the specific field value = extract_path(record, field_path) @cache[cache_key] = value value end rescue KeyError raise MissingReferenceError.new(schema_name: schema_name, field_path: field_path) ensure @mutex.synchronize do @generating.delete(schema_name) end end end |
#topological_sort(graph) ⇒ Array<String> (private)
Topologically sort schemas by dependencies
Orders schemas so that dependencies come before dependents. Uses Kahn's algorithm for topological sorting.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/synthra/generator/resolver.rb', line 186 def topological_sort(graph) in_degree = Hash.new(0) all_nodes = Set.new(@registry.names) graph.each do |node, deps| deps.each { |dep| in_degree[dep] += 1 } end queue = all_nodes.select { |n| in_degree[n].zero? } result = [] until queue.empty? node = queue.shift result << node graph[node].each do |dep| in_degree[dep] -= 1 queue << dep if in_degree[dep].zero? end end # If we couldn't sort all, there's a cycle - just return all names result.length == all_nodes.length ? result : @registry.names end |