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
Class Method Details
.extract_expr_from_source_location(source_location) ⇒ Object
80 81 82 83 84 85 86 87 88 89 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 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 80 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 = IO.readlines(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) 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 |
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 |
# 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 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
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 179 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
158 159 160 161 162 163 164 165 166 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 158 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
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 45 def interpret_derived_proc(derived) src, requireds, optionals, keywords, proc_line, = DerivedRules.extract_expr_from_source_location(derived.source_location) src = src.body&.slice || '' @src = src.lines.map(&:strip) 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(', ')}){ #{src}}", # rubocop:disable:next-line derived.source_location.first, # rubocop:enable derived.source_location.last.succ.succ ) } end |
#map_derived_inputs_to_paths(inputs) ⇒ Object
197 198 199 200 201 202 203 204 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 197 def map_derived_inputs_to_paths(inputs) inputs.values.map.with_index do |path, idx| segment_indexes = path.parts.map.with_index do |segment, i| segment.is_a?(PathParser::PathGroup) && segment..length == 1 ? i : nil end.compact [path, idx, segment_indexes.any? ? segment_indexes : nil] end end |
#parse_args_with_conditions(requireds, optionals_with_conditions, keywords_with_conditions) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 65 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
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 131 def parse_derived_inputs(derived) inputs = derived[:inputs] case inputs when Symbol, String if inputs =~ PLACEHOLDER_VAR_REGEX && !derived[:calc] @src ||= inputs input_hash = {} @input_mapper = {} derived[: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 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
168 169 170 171 172 173 174 175 176 177 |
# File 'lib/lazy_graph/node/derived_rules.rb', line 168 def parse_rule_string(derived) calc_str = derived[:calc] src = @src instance_eval( "->{ begin; #{calc_str}; rescue StandardError => e; LazyGraph.logger.error(\"Exception in \#{src}. \#{e.message}\"); LazyGraph.logger.error(e.backtrace.join(\"\\n\")); raise; end }", __FILE__, __LINE__ ) rescue SyntaxError missing_value = MissingValue { "Syntax error in #{derived[:src]}" } -> { missing_value } end |