Module: LazyGraph::Node::DerivedRules
- Included in:
- LazyGraph::Node
- Defined in:
- lib/lazy_graph/node/derived_rules.rb
Constant Summary collapse
- PLACEHOLDER_VAR_REGEX =
/\$\{[^}]+\}/
Class Method Summary collapse
Instance Method Summary collapse
-
#build_derived_inputs(derived, helpers) ⇒ Object
Derived input rules can be provided in a wide variety of formats, this function handles them all.
- #create_derived_input_context(derived, helpers) ⇒ Object
- #extract_derived_src(derived) ⇒ Object
- #interpret_derived_proc(derived) ⇒ Object
- #map_derived_inputs_to_paths(inputs) ⇒ Object
- #parse_args_with_conditions(requireds, optionals_with_conditions, keywords_with_conditions) ⇒ Object
- #parse_derived_inputs(derived) ⇒ Object
- #parse_rule_string(derived) ⇒ Object
- #resolver_for(path) ⇒ Object
- #rule_definition_backtrace ⇒ Object
Class Method Details
.extract_expr_from_source_location(source_location) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 90 def self.extract_expr_from_source_location(source_location) @derived_proc_cache ||= {} mtime = File.mtime(source_location.first).to_i if @derived_proc_cache[source_location]&.last.to_i.< mtime @derived_proc_cache[source_location] = begin source_lines = get_file_body(source_location.first) proc_line = source_location.last - 1 first_line = source_lines[proc_line] until first_line =~ /(?:lambda|proc|->)/ || proc_line.zero? proc_line -= 1 first_line = source_lines[proc_line] end lines = source_lines[proc_line..] lines[0] = lines[0][/(?:lambda|proc|->).*/] src_str = ''.dup intermediate = nil lines.each do |line| token_count = 0 line.split(/(?=\s|;|\)|\})/).each do |token| src_str << token token_count += 1 intermediate = Prism.parse(src_str) next unless intermediate.success? && token_count > 1 break end break if intermediate.success? end raise 'Source Extraction Failed' unless intermediate.success? src = intermediate.value.statements.body.first.yield_self do |s| s.type == :call_node ? s.block : s end requireds = (src.parameters&.parameters&.requireds || []).map(&:name) optionals = src.parameters&.parameters&.optionals || [] keywords = (src.parameters&.parameters&.keywords || []).map do |kw| [kw.name, kw.value.slice.gsub(/^_\./, '$.')] end.to_h [src, requireds, optionals, keywords, proc_line, mtime] end end @derived_proc_cache[source_location] rescue StandardError => e LazyGraph.logger.error(e.) LazyGraph.logger.error(e.backtrace.join("\n")) raise "Failed to extract expression from source location: #{source_location}. Ensure the file exists and the line number is correct. Extraction from a REPL is not supported" end |
.get_file_body(file_path) ⇒ Object
82 83 84 85 86 87 88 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 82 def self.get_file_body(file_path) @file_body_cache ||= {} if @file_body_cache[file_path]&.last.to_i < File.mtime(file_path).to_i @file_body_cache[file_path] = [IO.readlines(file_path), File.mtime(file_path).to_i] end @file_body_cache[file_path]&.first end |
Instance Method Details
#build_derived_inputs(derived, helpers) ⇒ Object
Derived input rules can be provided in a wide variety of formats, this function handles them all.
-
A simple string or symbol: ‘a.b.c’. The value at the nodes is simply set to the resolved value
-
Alternatively, you must split the inputs and the rule.
derived[:inputs]
a. Inputs as strings or symbols, e.g. inputs: ['position', 'velocity'].
These paths are resolved and made available within the rule by the same name
b. Inputs as a map of key-value pairs, e.g. inputs: { position: 'a.b.c', velocity: 'd.e.f' },
These are resolved and made available within the rule by the mapped name
The rule can be a simple string of Ruby code OR (this way we can encode entire lazy graphs as pure JSON)
A ruby block.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 25 def build_derived_inputs(derived, helpers) @resolvers = {}.compare_by_identity @path_cache = {}.compare_by_identity @resolution_stack = {}.compare_by_identity derived = interpret_derived_proc(derived) if derived.is_a?(Proc) derived = { inputs: derived.to_s } if derived.is_a?(String) || derived.is_a?(Symbol) derived[:inputs] = parse_derived_inputs(derived) @fixed_result = derived[:fixed_result] @copy_input = true if !derived[:calc] && derived[:inputs].size == 1 extract_derived_src(derived) if @debug @inputs_optional = derived[:calc].is_a?(Proc) derived[:calc] = parse_rule_string(derived) if derived[:calc].is_a?(String) || derived[:calc].is_a?(Symbol) @node_context = create_derived_input_context(derived, helpers) @inputs = map_derived_inputs_to_paths(derived[:inputs]) @conditions = derived[:conditions] @derived = true end |
#create_derived_input_context(derived, helpers) ⇒ Object
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 200 def create_derived_input_context(derived, helpers) return if @copy_input Struct.new(*(derived[:inputs].keys.map(&:to_sym) + %i[itself stack_ptr])) do def missing?(value) = value.is_a?(LazyGraph::MissingValue) || value.nil? helpers&.each { |h| include h } define_method(:process!, &derived[:calc]) if derived[:calc].is_a?(Proc) def method_missing(name, *args, &block) stack_ptr.send(name, *args, &block) end def respond_to_missing?(name, include_private = false) stack_ptr.respond_to?(name, include_private) end end.new end |
#extract_derived_src(derived) ⇒ Object
169 170 171 172 173 174 175 176 177 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 169 def extract_derived_src(derived) return @src ||= derived[:calc].to_s.lines unless derived[:calc].is_a?(Proc) @src ||= begin extract_expr_from_source_location(derived[:calc].source_location).body.slice.lines.map(&:strip) rescue StandardError ["Failed to extract source from proc #{derived}"] end end |
#interpret_derived_proc(derived) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 46 def interpret_derived_proc(derived) src, requireds, optionals, keywords, loc = DerivedRules.extract_expr_from_source_location(derived.source_location) body = src.body&.slice || '' @src = body.lines.map(&:strip) offset = src.slice.lines.length - body.lines.length inputs, conditions = parse_args_with_conditions(requireds, optionals, keywords) { inputs: inputs, mtime: File.mtime(derived.source_location.first), conditions: conditions, calc: instance_eval( "->(#{inputs.keys.map { |k| "#{k}=self.#{k}" }.join(', ')}){ #{body}}", # rubocop:disable:next-line derived.source_location.first, # rubocop:enable derived.source_location.last + offset ) } end |
#map_derived_inputs_to_paths(inputs) ⇒ Object
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 237 def map_derived_inputs_to_paths(inputs) inputs.values.map.with_index do |path, idx| segments = path.parts.map.with_index do |segment, i| if segment.is_a?(PathParser::PathGroup) && segment..length == 1 && !((resolver = resolver_for(segment..first)) || segment..first.segment.part.to_s =~ /\d+/) raise(ValidationError.new( "Invalid dependency in #{@path}: #{segment..first.to_path_str} cannot be resolved." ).tap { |e| e.set_backtrace(rule_definition_backtrace) }) end resolver ? [i, resolver] : nil end.compact resolver = resolver_for(path) unless resolver raise(ValidationError.new( "Invalid dependency in #{@path}: #{path.to_path_str} cannot be resolved." ).tap { |e| e.set_backtrace(rule_definition_backtrace) }) end [path, resolver, idx, segments.any? ? segments : nil] end end |
#parse_args_with_conditions(requireds, optionals_with_conditions, keywords_with_conditions) ⇒ Object
67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 67 def parse_args_with_conditions(requireds, optionals_with_conditions, keywords_with_conditions) keywords = requireds.map { |r| [r, r] }.to_h conditions = {} keywords_with_conditions.map do |k, v| path, condition = v.split('=') keywords[k] = path conditions[k] = eval(condition) if condition end optionals_with_conditions.each do |optional_with_conditions| keywords[optional_with_conditions.name] = optional_with_conditions.name conditions[optional_with_conditions.name] = eval(optional_with_conditions.value.slice) end [keywords, conditions.any? ? conditions : nil] end |
#parse_derived_inputs(derived) ⇒ Object
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 141 def parse_derived_inputs(derived) inputs = derived[:inputs] case inputs when Symbol, String if !derived[:calc] @src ||= inputs input_hash = {} @input_mapper = {} calc = inputs.gsub(PLACEHOLDER_VAR_REGEX) do |match| sub = input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}" @input_mapper[sub.to_sym] = match[2...-1].to_sym sub end derived[:calc] = calc unless calc == input_hash.values.first input_hash.invert else { inputs.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => inputs.to_s.freeze } end when Array pairs = inputs.last.is_a?(Hash) ? inputs.pop : {} inputs.map { |v| { v.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => v } }.reduce(pairs, :merge) when Hash inputs else {} end.transform_values { |v| PathParser.parse(v) } end |
#parse_rule_string(derived) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 179 def parse_rule_string(derived) calc_str = derived[:calc] node_path = path src = <<~RUBY, @rule_location&.first, @rule_location&.last.to_i - 2 ->{ begin #{calc_str} rescue StandardError => e; LazyGraph.logger.error("Exception in \#{calc_str} => \#{node_path}. \#{e.message}") raise e end } RUBY instance_eval(*src) rescue SyntaxError missing_value = MissingValue { "Syntax error in #{derived[:src]}" } -> { missing_value } end |
#resolver_for(path) ⇒ Object
218 219 220 221 222 223 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 218 def resolver_for(path) segment = path.segment.part return root.properties[path.next.segment.part] if segment == :'$' (segment == name ? parent.parent : @parent).find_resolver_for(segment) end |
#rule_definition_backtrace ⇒ Object
225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 225 def rule_definition_backtrace if @rule_location && @rule_location.size >= 2 rule_file, rule_line = @rule_location rule_entry = "#{rule_file}:#{rule_line}:in `rule`" else rule_entry = 'unknown_rule_location' end current_backtrace = caller.reverse.take_while { |line| !line.include?('/lib/lazy_graph/') }.reverse [rule_entry] + current_backtrace end |