lazy_graph 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|