lazy_graph 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +81 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +562 -0
- data/Rakefile +4 -0
- data/examples/performance_tests.rb +117 -0
- data/lib/lazy_graph/builder/dsl.rb +315 -0
- data/lib/lazy_graph/builder.rb +138 -0
- data/lib/lazy_graph/builder_group.rb +57 -0
- data/lib/lazy_graph/context.rb +60 -0
- data/lib/lazy_graph/graph.rb +73 -0
- data/lib/lazy_graph/hash_utils.rb +87 -0
- data/lib/lazy_graph/lazy-graph.json +148 -0
- data/lib/lazy_graph/missing_value.rb +26 -0
- data/lib/lazy_graph/node/array_node.rb +67 -0
- data/lib/lazy_graph/node/derived_rules.rb +196 -0
- data/lib/lazy_graph/node/node_properties.rb +64 -0
- data/lib/lazy_graph/node/object_node.rb +113 -0
- data/lib/lazy_graph/node.rb +316 -0
- data/lib/lazy_graph/path_parser/path.rb +46 -0
- data/lib/lazy_graph/path_parser/path_group.rb +12 -0
- data/lib/lazy_graph/path_parser/path_part.rb +13 -0
- data/lib/lazy_graph/path_parser.rb +211 -0
- data/lib/lazy_graph/server.rb +86 -0
- data/lib/lazy_graph/stack_pointer.rb +56 -0
- data/lib/lazy_graph/version.rb +5 -0
- data/lib/lazy_graph.rb +32 -0
- data/logo.png +0 -0
- metadata +200 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyGraph
|
4
|
+
module HashUtils
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def deep_dup!(hash)
|
8
|
+
case hash
|
9
|
+
when Hash
|
10
|
+
hash = hash.dup
|
11
|
+
hash.each do |key, value|
|
12
|
+
hash[key] = deep_dup!(value)
|
13
|
+
end
|
14
|
+
when Array
|
15
|
+
hash.map { |value| deep_dup!(value) }
|
16
|
+
end
|
17
|
+
hash
|
18
|
+
end
|
19
|
+
|
20
|
+
def deep_merge(hash, other_hash, path = :'')
|
21
|
+
hash.merge(other_hash.transform_keys(&:to_sym)) do |key, this_val, other_val|
|
22
|
+
if this_val.is_a?(Hash) && other_val.is_a?(Hash) && other_val != this_val
|
23
|
+
deep_merge(this_val, other_val, :"#{path}.#{key}")
|
24
|
+
elsif this_val.is_a?(Array) && other_val.is_a?(Array) && other_val != this_val
|
25
|
+
this_val.concat(other_val).uniq
|
26
|
+
else
|
27
|
+
if this_val != other_val && !(this_val.is_a?(Proc) && other_val.is_a?(Proc))
|
28
|
+
LazyGraph.logger.warn("Warning: Conflicting values at #{path}.#{key}. #{this_val} != #{other_val} ")
|
29
|
+
end
|
30
|
+
other_val
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def strip_invalid(obj, parent_list = {}.compare_by_identity)
|
36
|
+
return { '^ref': :circular } if (circular_dependency = parent_list[obj])
|
37
|
+
|
38
|
+
parent_list[obj] = true
|
39
|
+
case obj
|
40
|
+
when Hash
|
41
|
+
obj.each_with_object({}) do |(key, value), obj|
|
42
|
+
next if value.is_a?(MissingValue)
|
43
|
+
|
44
|
+
obj[key] = strip_invalid(value, parent_list)
|
45
|
+
end
|
46
|
+
when Struct
|
47
|
+
obj.members.each_with_object({}) do |key, res|
|
48
|
+
next if obj[key].is_a?(MissingValue)
|
49
|
+
next if obj.invisible.include?(key)
|
50
|
+
|
51
|
+
res[key] = strip_invalid(obj[key], parent_list)
|
52
|
+
end
|
53
|
+
when Array
|
54
|
+
obj.map! { |value| strip_invalid(value, parent_list) }
|
55
|
+
else
|
56
|
+
obj
|
57
|
+
end
|
58
|
+
ensure
|
59
|
+
parent_list.delete(obj) unless circular_dependency
|
60
|
+
end
|
61
|
+
|
62
|
+
def deep_symbolize!(obj)
|
63
|
+
case obj
|
64
|
+
when Hash
|
65
|
+
hash = 0
|
66
|
+
obj.to_a.each do |key, value|
|
67
|
+
hash ^= deep_symbolize!(value)
|
68
|
+
unless key.is_a?(Symbol)
|
69
|
+
key.to_s.to_sym
|
70
|
+
obj[key.to_s.to_sym] = obj.delete(key)
|
71
|
+
end
|
72
|
+
hash ^= key.object_id
|
73
|
+
end
|
74
|
+
obj.compare_by_identity
|
75
|
+
hash
|
76
|
+
when Array
|
77
|
+
hash = 0
|
78
|
+
obj.each { |item| hash ^= deep_symbolize!(item) }
|
79
|
+
hash
|
80
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
81
|
+
obj.hash
|
82
|
+
else
|
83
|
+
0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "http://json-schema.org/draft-06/schema#",
|
3
|
+
"$id": "http://json-schema.org/draft-06/schema#",
|
4
|
+
"title": "Core schema meta-schema",
|
5
|
+
"definitions": {
|
6
|
+
"schemaArray": {
|
7
|
+
"type": "array",
|
8
|
+
"minItems": 1,
|
9
|
+
"items": { "$ref": "#" }
|
10
|
+
},
|
11
|
+
"nonNegativeInteger": {
|
12
|
+
"type": "integer",
|
13
|
+
"minimum": 0
|
14
|
+
},
|
15
|
+
"nonNegativeIntegerDefault0": {
|
16
|
+
"allOf": [
|
17
|
+
{ "$ref": "#/definitions/nonNegativeInteger" },
|
18
|
+
{ "default": 0 }
|
19
|
+
]
|
20
|
+
},
|
21
|
+
"simpleTypes": {
|
22
|
+
"enum": [
|
23
|
+
"array",
|
24
|
+
"boolean",
|
25
|
+
"integer",
|
26
|
+
"null",
|
27
|
+
"number",
|
28
|
+
"object",
|
29
|
+
"string",
|
30
|
+
"decimal",
|
31
|
+
"timestamp",
|
32
|
+
"date",
|
33
|
+
"time"
|
34
|
+
]
|
35
|
+
},
|
36
|
+
"stringArray": {
|
37
|
+
"type": "array",
|
38
|
+
"items": { "type": "string" },
|
39
|
+
"uniqueItems": true,
|
40
|
+
"default": []
|
41
|
+
}
|
42
|
+
},
|
43
|
+
"type": ["object", "boolean"],
|
44
|
+
"properties": {
|
45
|
+
"$id": {
|
46
|
+
"type": "string",
|
47
|
+
"format": "uri-reference"
|
48
|
+
},
|
49
|
+
"$schema": {
|
50
|
+
"type": "string",
|
51
|
+
"format": "uri"
|
52
|
+
},
|
53
|
+
"$ref": {
|
54
|
+
"type": "string",
|
55
|
+
"format": "uri-reference"
|
56
|
+
},
|
57
|
+
"title": {
|
58
|
+
"type": "string"
|
59
|
+
},
|
60
|
+
"description": {
|
61
|
+
"type": "string"
|
62
|
+
},
|
63
|
+
"default": {},
|
64
|
+
"multipleOf": {
|
65
|
+
"type": "number",
|
66
|
+
"exclusiveMinimum": 0
|
67
|
+
},
|
68
|
+
"maximum": {
|
69
|
+
"type": "number"
|
70
|
+
},
|
71
|
+
"exclusiveMaximum": {
|
72
|
+
"type": "number"
|
73
|
+
},
|
74
|
+
"minimum": {
|
75
|
+
"type": "number"
|
76
|
+
},
|
77
|
+
"exclusiveMinimum": {
|
78
|
+
"type": "number"
|
79
|
+
},
|
80
|
+
"maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
|
81
|
+
"minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
|
82
|
+
"pattern": {
|
83
|
+
"type": "string",
|
84
|
+
"format": "regex"
|
85
|
+
},
|
86
|
+
"additionalItems": { "$ref": "#" },
|
87
|
+
"items": {
|
88
|
+
"anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/schemaArray" }],
|
89
|
+
"default": {}
|
90
|
+
},
|
91
|
+
"maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
|
92
|
+
"minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
|
93
|
+
"uniqueItems": {
|
94
|
+
"type": "boolean",
|
95
|
+
"default": false
|
96
|
+
},
|
97
|
+
"contains": { "$ref": "#" },
|
98
|
+
"maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
|
99
|
+
"minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
|
100
|
+
"required": { "$ref": "#/definitions/stringArray" },
|
101
|
+
"additionalProperties": { "$ref": "#" },
|
102
|
+
"definitions": {
|
103
|
+
"type": "object",
|
104
|
+
"additionalProperties": { "$ref": "#" },
|
105
|
+
"default": {}
|
106
|
+
},
|
107
|
+
"properties": {
|
108
|
+
"type": "object",
|
109
|
+
"additionalProperties": { "$ref": "#" },
|
110
|
+
"default": {}
|
111
|
+
},
|
112
|
+
"patternProperties": {
|
113
|
+
"type": "object",
|
114
|
+
"additionalProperties": { "$ref": "#" },
|
115
|
+
"default": {}
|
116
|
+
},
|
117
|
+
"dependencies": {
|
118
|
+
"type": "object",
|
119
|
+
"additionalProperties": {
|
120
|
+
"anyOf": [{ "$ref": "#" }, { "$ref": "#/definitions/stringArray" }]
|
121
|
+
}
|
122
|
+
},
|
123
|
+
"propertyNames": { "$ref": "#" },
|
124
|
+
"const": {},
|
125
|
+
"enum": {
|
126
|
+
"type": "array",
|
127
|
+
"minItems": 1,
|
128
|
+
"uniqueItems": true
|
129
|
+
},
|
130
|
+
"type": {
|
131
|
+
"anyOf": [
|
132
|
+
{ "$ref": "#/definitions/simpleTypes" },
|
133
|
+
{
|
134
|
+
"type": "array",
|
135
|
+
"items": { "$ref": "#/definitions/simpleTypes" },
|
136
|
+
"minItems": 1,
|
137
|
+
"uniqueItems": true
|
138
|
+
}
|
139
|
+
]
|
140
|
+
},
|
141
|
+
"format": { "type": "string" },
|
142
|
+
"allOf": { "$ref": "#/definitions/schemaArray" },
|
143
|
+
"anyOf": { "$ref": "#/definitions/schemaArray" },
|
144
|
+
"oneOf": { "$ref": "#/definitions/schemaArray" },
|
145
|
+
"not": { "$ref": "#" }
|
146
|
+
},
|
147
|
+
"default": {}
|
148
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyGraph
|
4
|
+
# Represents a value that is missing or undefined, allowing for graceful handling
|
5
|
+
# of method calls and JSON serialization even when the value is not present.
|
6
|
+
class MissingValue
|
7
|
+
attr_reader :details
|
8
|
+
|
9
|
+
def initialize(details) = @details = details
|
10
|
+
def to_s = "MISSING[#{@details}]"
|
11
|
+
|
12
|
+
def inspect = to_s
|
13
|
+
def coerce(other) = [self, other]
|
14
|
+
def as_json = nil
|
15
|
+
def respond_to_missing?(_method_name, _include_private = false) = true
|
16
|
+
|
17
|
+
def method_missing(method, *args, &block)
|
18
|
+
return super if method == :to_ary
|
19
|
+
return self if self == BLANK
|
20
|
+
|
21
|
+
MissingValue.new(:"#{details}##{method}#{args.any? ? :"(#{args.inspect[1...-1]})" : :''}")
|
22
|
+
end
|
23
|
+
|
24
|
+
BLANK = new('')
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module LazyGraph
|
2
|
+
class ArrayNode < Node
|
3
|
+
# An Array supports the following types of path resolutions.
|
4
|
+
# 1. Forward property (assuming child items are objects): arr.property
|
5
|
+
# 2. Absolute Index: arr[0], arr[1], arr[2], ...
|
6
|
+
# 3. Range: arr[0..2], arr[1...3]
|
7
|
+
# 4. All:arr [*]
|
8
|
+
# 5. Set of indexes: arr[0, 2, 4]
|
9
|
+
#
|
10
|
+
# Parts between square brackets are represented as path groups.
|
11
|
+
def resolve(
|
12
|
+
path,
|
13
|
+
stack_memory,
|
14
|
+
should_recycle = stack_memory,
|
15
|
+
**
|
16
|
+
)
|
17
|
+
input = stack_memory.frame
|
18
|
+
@visited[input.object_id ^ (path.object_id << 8)] ||= begin
|
19
|
+
if (path_segment = path.segment).is_a?(PathParser::PathGroup)
|
20
|
+
unless path_segment.options.all?(&:index?)
|
21
|
+
return input.length.times.map do |index|
|
22
|
+
item = children.fetch_item(input, index, stack_memory)
|
23
|
+
children.resolve(path, stack_memory.push(item, index))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
return resolve(path_segment.options.first.merge(path.next), stack_memory, nil) if path_segment.options.one?
|
28
|
+
|
29
|
+
return path_segment.options.map { |part| resolve(part.merge(path.next), stack_memory, nil) }
|
30
|
+
end
|
31
|
+
|
32
|
+
segment = path_segment&.part
|
33
|
+
case segment
|
34
|
+
when nil
|
35
|
+
input.length.times do |index|
|
36
|
+
item = children.fetch_item(input, index, stack_memory)
|
37
|
+
children.resolve(path, stack_memory.push(item, index))
|
38
|
+
end
|
39
|
+
input
|
40
|
+
when DIGIT_REGEXP
|
41
|
+
item = @children.fetch_item(input, segment.to_s.to_i, stack_memory)
|
42
|
+
children.resolve(path.next, stack_memory.push(item, segment))
|
43
|
+
when :*
|
44
|
+
input.length.times.map do |index|
|
45
|
+
item = children.fetch_item(input, index, stack_memory)
|
46
|
+
@children.resolve(path.next, stack_memory.push(item, index))
|
47
|
+
end
|
48
|
+
else
|
49
|
+
if @children.is_object && @children.children[:properties].keys.include?(segment) || input&.first&.key?(segment)
|
50
|
+
input.length.times.map do |index|
|
51
|
+
item = children.fetch_item(input, index, stack_memory)
|
52
|
+
@children.resolve(path, stack_memory.push(item, index))
|
53
|
+
end
|
54
|
+
else
|
55
|
+
MissingValue()
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
should_recycle&.recycle!
|
61
|
+
end
|
62
|
+
|
63
|
+
def cast(value)
|
64
|
+
value.dup
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prism'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module LazyGraph
|
7
|
+
class Node
|
8
|
+
module DerivedRules
|
9
|
+
PLACEHOLDER_VAR_REGEX = /\$\{[^}]+\}/
|
10
|
+
# Derived input rules can be provided in a wide variety of formats,
|
11
|
+
# this function handles them all.
|
12
|
+
#
|
13
|
+
# 1. A simple string or symbol: 'a.b.c'. The value at the nodes is simply set to the resolved value
|
14
|
+
#
|
15
|
+
# 2. Alternatively, you must split the inputs and the rule.
|
16
|
+
# derived[:inputs]
|
17
|
+
# a. Inputs as strings or symbols, e.g. inputs: ['position', 'velocity'].
|
18
|
+
# These paths are resolved and made available within the rule by the same name
|
19
|
+
# b. Inputs as a map of key-value pairs, e.g. inputs: { position: 'a.b.c', velocity: 'd.e.f' },
|
20
|
+
# These are resolved and made available within the rule by the mapped name
|
21
|
+
#
|
22
|
+
# 3. derived[:calc]
|
23
|
+
# The rule can be a simple string of Ruby code OR (this way we can encode entire lazy graphs as pure JSON)
|
24
|
+
# A ruby block.
|
25
|
+
def build_derived_inputs(derived, helpers)
|
26
|
+
@resolvers = {}.compare_by_identity
|
27
|
+
@path_cache = {}.compare_by_identity
|
28
|
+
|
29
|
+
derived = interpret_derived_proc(derived) if derived.is_a?(Proc)
|
30
|
+
derived = { inputs: derived.to_s } if derived.is_a?(String) || derived.is_a?(Symbol)
|
31
|
+
derived[:inputs] = parse_derived_inputs(derived)
|
32
|
+
@copy_input = true if !derived[:calc] && derived[:inputs].size == 1
|
33
|
+
extract_derived_src(derived) if @debug
|
34
|
+
|
35
|
+
@inputs_optional = derived[:calc].is_a?(Proc)
|
36
|
+
derived[:calc] = parse_rule_string(derived) if derived[:calc].is_a?(String) || derived[:calc].is_a?(Symbol)
|
37
|
+
|
38
|
+
@node_context = create_derived_input_context(derived, helpers)
|
39
|
+
@inputs = map_derived_inputs_to_paths(derived[:inputs])
|
40
|
+
@conditions = derived[:conditions]
|
41
|
+
@derived = true
|
42
|
+
end
|
43
|
+
|
44
|
+
def interpret_derived_proc(derived)
|
45
|
+
src, requireds, optionals, keywords, = DerivedRules.extract_expr_from_source_location(derived.source_location)
|
46
|
+
src = src.body&.slice || ''
|
47
|
+
@src = src.lines.map(&:strip)
|
48
|
+
inputs, conditions = parse_args_with_conditions(requireds, optionals, keywords)
|
49
|
+
|
50
|
+
{
|
51
|
+
inputs: inputs,
|
52
|
+
mtime: File.mtime(derived.source_location.first),
|
53
|
+
conditions: conditions,
|
54
|
+
calc: instance_eval(
|
55
|
+
"->(#{inputs.keys.map { |k| "#{k}=self.#{k}" }.join(', ')}){ #{src}}",
|
56
|
+
# rubocop:disable:next-line
|
57
|
+
derived.source_location.first,
|
58
|
+
# rubocop:enable
|
59
|
+
derived.source_location.last.succ
|
60
|
+
)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_args_with_conditions(requireds, optionals_with_conditions, keywords_with_conditions)
|
65
|
+
keywords = requireds.map { |r| [r, r] }.to_h
|
66
|
+
conditions = {}
|
67
|
+
keywords_with_conditions.map do |k, v|
|
68
|
+
path, condition = v.split('=')
|
69
|
+
keywords[k] = path
|
70
|
+
conditions[k] = eval(condition) if condition
|
71
|
+
end
|
72
|
+
optionals_with_conditions.each do |optional_with_conditions|
|
73
|
+
keywords[optional_with_conditions.name] = optional_with_conditions.name
|
74
|
+
conditions[optional_with_conditions.name] = eval(optional_with_conditions.value.slice)
|
75
|
+
end
|
76
|
+
[keywords, conditions.any? ? conditions : nil]
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.extract_expr_from_source_location(source_location)
|
80
|
+
@derived_proc_cache ||= {}
|
81
|
+
mtime = File.mtime(source_location.first).to_i
|
82
|
+
|
83
|
+
if @derived_proc_cache[source_location]&.last.to_i.< mtime
|
84
|
+
@derived_proc_cache[source_location] = begin
|
85
|
+
source_lines = IO.readlines(source_location.first)
|
86
|
+
proc_line = source_location.last - 1
|
87
|
+
first_line = source_lines[proc_line]
|
88
|
+
until first_line =~ /(?:lambda|proc|->)/ || proc_line.zero?
|
89
|
+
proc_line -= 1
|
90
|
+
first_line = source_lines[proc_line]
|
91
|
+
end
|
92
|
+
lines = source_lines[proc_line..]
|
93
|
+
lines[0] = lines[0][/(?:lambda|proc|->).*/]
|
94
|
+
src_str = ''.dup
|
95
|
+
intermediate = nil
|
96
|
+
lines.each do |line|
|
97
|
+
token_count = 0
|
98
|
+
line.split(/(?=\s|;|\)|\})/).each do |token|
|
99
|
+
src_str << token
|
100
|
+
token_count += 1
|
101
|
+
intermediate = Prism.parse(src_str)
|
102
|
+
next unless intermediate.success? && token_count > 1
|
103
|
+
|
104
|
+
break
|
105
|
+
end
|
106
|
+
break if intermediate.success?
|
107
|
+
end
|
108
|
+
|
109
|
+
raise 'Source Extraction Failed' unless intermediate.success?
|
110
|
+
|
111
|
+
src = intermediate.value.statements.body.first.yield_self do |s|
|
112
|
+
s.type == :call_node ? s.block : s
|
113
|
+
end
|
114
|
+
requireds = (src.parameters&.parameters&.requireds || []).map(&:name)
|
115
|
+
optionals = src.parameters&.parameters&.optionals || []
|
116
|
+
keywords = (src.parameters&.parameters&.keywords || []).map do |kw|
|
117
|
+
[kw.name, kw.value.slice.gsub(/^_\./, '$.')]
|
118
|
+
end.to_h
|
119
|
+
[src, requireds, optionals, keywords, mtime]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
@derived_proc_cache[source_location]
|
124
|
+
rescue StandardError => e
|
125
|
+
LazyGraph.logger.error(e.message)
|
126
|
+
LazyGraph.logger.error(e.backtrace)
|
127
|
+
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"
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_derived_inputs(derived)
|
131
|
+
inputs = derived[:inputs]
|
132
|
+
case inputs
|
133
|
+
when Symbol, String
|
134
|
+
if inputs =~ PLACEHOLDER_VAR_REGEX && !derived[:calc]
|
135
|
+
input_hash = {}
|
136
|
+
derived[:calc] = inputs.gsub(PLACEHOLDER_VAR_REGEX) do |match|
|
137
|
+
input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}"
|
138
|
+
end
|
139
|
+
input_hash.invert
|
140
|
+
else
|
141
|
+
{ inputs.to_str.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => inputs.to_str.freeze }
|
142
|
+
end
|
143
|
+
when Array
|
144
|
+
inputs.map { |v| { v.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => v } }.reduce({}, :merge)
|
145
|
+
when Hash
|
146
|
+
inputs
|
147
|
+
else
|
148
|
+
{}
|
149
|
+
end.transform_values { |v| PathParser.parse(v) }
|
150
|
+
end
|
151
|
+
|
152
|
+
def extract_derived_src(derived)
|
153
|
+
return @src = derived[:calc].to_s.lines unless derived[:calc].is_a?(Proc)
|
154
|
+
|
155
|
+
@src ||= begin
|
156
|
+
extract_expr_from_source_location(derived[:calc].source_location).body.slice.lines.map(&:strip)
|
157
|
+
rescue StandardError
|
158
|
+
["Failed to extract source from proc #{derived}"]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_rule_string(derived)
|
163
|
+
instance_eval("->{ #{derived[:calc]} }", __FILE__, __LINE__)
|
164
|
+
rescue SyntaxError
|
165
|
+
missing_value = MissingValue { "Syntax error in #{derived[:src]}" }
|
166
|
+
-> { missing_value }
|
167
|
+
end
|
168
|
+
|
169
|
+
def create_derived_input_context(derived, helpers)
|
170
|
+
return if @copy_input
|
171
|
+
|
172
|
+
Struct.new(*(derived[:inputs].keys.map(&:to_sym) + %i[itself stack_ptr])) do
|
173
|
+
def missing?(value) = value.is_a?(LazyGraph::MissingValue) || value.nil?
|
174
|
+
helpers&.each { |h| include h }
|
175
|
+
define_method(:process!, &derived[:calc])
|
176
|
+
def method_missing(name, *args, &block)
|
177
|
+
stack_ptr.send(name, *args, &block)
|
178
|
+
end
|
179
|
+
|
180
|
+
def respond_to_missing?(name, include_private = false)
|
181
|
+
stack_ptr.respond_to?(name, include_private)
|
182
|
+
end
|
183
|
+
end.new
|
184
|
+
end
|
185
|
+
|
186
|
+
def map_derived_inputs_to_paths(inputs)
|
187
|
+
inputs.values.map.with_index do |path, idx|
|
188
|
+
segment_indexes = path.parts.map.with_index do |segment, i|
|
189
|
+
segment.is_a?(PathParser::PathGroup) && segment.options.length == 1 ? i : nil
|
190
|
+
end.compact
|
191
|
+
[path, idx, segment_indexes.any? ? segment_indexes : nil]
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module LazyGraph
|
2
|
+
module NodeProperties
|
3
|
+
# Builds an Anonymous Struct with the given members
|
4
|
+
# Invisible members are ignored when the struct is serialized
|
5
|
+
def self.build(members:, invisible:)
|
6
|
+
Struct.new(*members, keyword_init: true) do
|
7
|
+
define_method(:initialize) do |kws|
|
8
|
+
members.each { |k| self[k] = kws[k] || MissingValue::BLANK }
|
9
|
+
end
|
10
|
+
|
11
|
+
define_method(:key?) do |x|
|
12
|
+
!self[x].equal?(MissingValue::BLANK)
|
13
|
+
rescue StandardError
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
define_method(:[]=) do |key, val|
|
18
|
+
super(key, val)
|
19
|
+
end
|
20
|
+
|
21
|
+
define_method(:members) do
|
22
|
+
members
|
23
|
+
end
|
24
|
+
|
25
|
+
define_method(:invisible) do
|
26
|
+
invisible
|
27
|
+
end
|
28
|
+
|
29
|
+
define_method(:each_key, &members.method(:each))
|
30
|
+
|
31
|
+
def dup
|
32
|
+
self.class.new(members.map do |k|
|
33
|
+
[k, self[k].dup]
|
34
|
+
end.to_h)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_first_of(*props)
|
38
|
+
key = props.find do |prop|
|
39
|
+
!self[prop].is_a?(MissingValue)
|
40
|
+
end
|
41
|
+
key ? self[key] : MissingValue::BLANK
|
42
|
+
end
|
43
|
+
|
44
|
+
def pretty_print(q)
|
45
|
+
# Start the custom pretty print
|
46
|
+
q.group(1, '<Props ', '>') do
|
47
|
+
q.seplist(members.zip(values).reject do |m, v|
|
48
|
+
m == :DEBUG && (v.nil? || v.is_a?(MissingValue))
|
49
|
+
end) do |member, value|
|
50
|
+
q.group do
|
51
|
+
q.text "#{member}="
|
52
|
+
if value.respond_to?(:pretty_print)
|
53
|
+
q.pp(value) # Delegate to the nested value's pretty_print
|
54
|
+
else
|
55
|
+
q.text value.inspect
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|