lazy_graph 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +282 -47
- data/Rakefile +12 -1
- data/examples/converter.rb +41 -0
- data/examples/performance_tests.rb +9 -9
- data/examples/shopping_cart_totals.rb +56 -0
- data/lib/lazy_graph/builder/dsl.rb +67 -30
- data/lib/lazy_graph/builder.rb +52 -25
- data/lib/lazy_graph/builder_group.rb +31 -18
- data/lib/lazy_graph/cli.rb +47 -0
- data/lib/lazy_graph/context.rb +37 -12
- data/lib/lazy_graph/environment.rb +6 -0
- data/lib/lazy_graph/graph.rb +28 -12
- data/lib/lazy_graph/hash_utils.rb +10 -5
- data/lib/lazy_graph/logger.rb +87 -0
- data/lib/lazy_graph/missing_value.rb +9 -2
- data/lib/lazy_graph/node/array_node.rb +14 -18
- data/lib/lazy_graph/node/derived_rules.rb +72 -17
- data/lib/lazy_graph/node/node_properties.rb +34 -9
- data/lib/lazy_graph/node/object_node.rb +41 -56
- data/lib/lazy_graph/node/symbol_hash.rb +15 -2
- data/lib/lazy_graph/node.rb +183 -94
- data/lib/lazy_graph/path_parser/path.rb +15 -7
- data/lib/lazy_graph/path_parser/path_group.rb +6 -0
- data/lib/lazy_graph/path_parser/path_part.rb +8 -0
- data/lib/lazy_graph/path_parser.rb +1 -1
- data/lib/lazy_graph/rack_app.rb +138 -0
- data/lib/lazy_graph/rack_server.rb +199 -0
- data/lib/lazy_graph/stack_pointer.rb +22 -14
- data/lib/lazy_graph/version.rb +1 -1
- data/lib/lazy_graph.rb +6 -8
- metadata +115 -11
- data/lib/lazy_graph/server.rb +0 -91
data/lib/lazy_graph/graph.rb
CHANGED
@@ -4,16 +4,18 @@ require 'json-schema'
|
|
4
4
|
|
5
5
|
module LazyGraph
|
6
6
|
# Represents a lazy graph structure based on JSON schema
|
7
|
-
VALIDATION_CACHE = {}
|
7
|
+
VALIDATION_CACHE = {}.compare_by_identity
|
8
8
|
METASCHEMA = JSON.load_file(File.join(__dir__, 'lazy-graph.json'))
|
9
9
|
|
10
10
|
class Graph
|
11
11
|
attr_reader :json_schema, :root_node, :validate
|
12
12
|
|
13
13
|
def context(input) = Context.new(self, input)
|
14
|
-
def debug? =
|
14
|
+
def debug? = !!@debug
|
15
|
+
alias input context
|
16
|
+
alias feed context
|
15
17
|
|
16
|
-
def initialize(input_schema, debug: false, validate: true, helpers: nil)
|
18
|
+
def initialize(input_schema, debug: false, validate: true, helpers: nil, namespace: nil)
|
17
19
|
@json_schema = HashUtils.deep_dup(input_schema, symbolize: true, signature: signature = [0]).merge(type: :object)
|
18
20
|
@debug = debug
|
19
21
|
@validate = validate
|
@@ -25,18 +27,20 @@ module LazyGraph
|
|
25
27
|
raise ArgumentError, 'Root schema must be a non-empty object'
|
26
28
|
end
|
27
29
|
|
28
|
-
@root_node = build_node(@json_schema)
|
30
|
+
@root_node = build_node(@json_schema, namespace: namespace)
|
31
|
+
@root_node.build_derived_inputs!
|
29
32
|
end
|
30
33
|
|
31
|
-
def build_node(schema, path = :'$', name = :root, parent = nil)
|
34
|
+
def build_node(schema, path = :'$', name = :root, parent = nil, namespace: nil)
|
32
35
|
schema[:type] = schema[:type].to_sym
|
33
|
-
node =
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
36
|
+
node = \
|
37
|
+
case schema[:type]
|
38
|
+
when :object then ObjectNode
|
39
|
+
when :array then ArrayNode
|
40
|
+
else Node
|
41
|
+
end.new(name, path, schema, parent, debug: @debug, helpers: @helpers, namespace: namespace)
|
38
42
|
|
39
|
-
if node.type
|
43
|
+
if node.type.equal?(:object)
|
40
44
|
node.children = \
|
41
45
|
{
|
42
46
|
properties: schema.fetch(:properties, {}).map do |key, value|
|
@@ -46,7 +50,7 @@ module LazyGraph
|
|
46
50
|
[Regexp.new(key.to_s), build_node(value, :"#{path}.#{key}", :'<property>', node)]
|
47
51
|
end
|
48
52
|
}
|
49
|
-
elsif node.type
|
53
|
+
elsif node.type.equal?(:array)
|
50
54
|
node.children = build_node(schema.fetch(:items, {}), :"#{path}[]", :items, node)
|
51
55
|
end
|
52
56
|
node
|
@@ -54,6 +58,18 @@ module LazyGraph
|
|
54
58
|
|
55
59
|
def validate!(input, schema = @json_schema)
|
56
60
|
JSON::Validator.validate!(schema, input)
|
61
|
+
rescue JSON::Schema::ValidationError => e
|
62
|
+
raise ValidationError, "Input validation failed: #{e.message}", cause: e
|
63
|
+
end
|
64
|
+
|
65
|
+
def pretty_print(q)
|
66
|
+
# Start the custom pretty print
|
67
|
+
q.group(1, '<LazyGraph::Graph ', '>') do
|
68
|
+
q.group do
|
69
|
+
q.text 'props='
|
70
|
+
q.text root_node.children[:properties].keys
|
71
|
+
end
|
72
|
+
end
|
57
73
|
end
|
58
74
|
end
|
59
75
|
|
@@ -20,6 +20,8 @@ module LazyGraph
|
|
20
20
|
signature[0] ^= key.object_id if signature
|
21
21
|
result[key] = deep_dup(value, symbolize: symbolize, signature: signature)
|
22
22
|
end
|
23
|
+
when Struct
|
24
|
+
deep_dup(obj.to_h, symbolize: symbolize, signature: signature)
|
23
25
|
when Array
|
24
26
|
obj.map { |value| deep_dup(value, symbolize: symbolize, signature: signature) }
|
25
27
|
when String, Numeric, TrueClass, FalseClass, NilClass
|
@@ -47,7 +49,7 @@ module LazyGraph
|
|
47
49
|
end
|
48
50
|
end
|
49
51
|
|
50
|
-
def
|
52
|
+
def strip_missing(obj, parent_list = {}.compare_by_identity)
|
51
53
|
return { '^ref': :circular } if (circular_dependency = parent_list[obj])
|
52
54
|
|
53
55
|
parent_list[obj] = true
|
@@ -55,18 +57,21 @@ module LazyGraph
|
|
55
57
|
when Hash
|
56
58
|
obj.each_with_object({}) do |(key, value), obj|
|
57
59
|
next if value.is_a?(MissingValue)
|
60
|
+
next if value.nil?
|
58
61
|
|
59
|
-
obj[key] =
|
62
|
+
obj[key] = strip_missing(value, parent_list)
|
60
63
|
end
|
61
64
|
when Struct
|
62
65
|
obj.members.each_with_object({}) do |key, res|
|
63
|
-
|
66
|
+
value = obj.original_get(key)
|
67
|
+
next if value.is_a?(MissingValue)
|
68
|
+
next if value.nil?
|
64
69
|
next if obj.invisible.include?(key)
|
65
70
|
|
66
|
-
res[key] =
|
71
|
+
res[key] = strip_missing(obj[key], parent_list)
|
67
72
|
end
|
68
73
|
when Array
|
69
|
-
obj.map { |value|
|
74
|
+
obj.map { |value| strip_missing(value, parent_list) }
|
70
75
|
when MissingValue
|
71
76
|
nil
|
72
77
|
else
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require_relative 'environment'
|
3
|
+
|
4
|
+
module LazyGraph
|
5
|
+
class << self
|
6
|
+
attr_accessor :logger
|
7
|
+
end
|
8
|
+
|
9
|
+
module Logger
|
10
|
+
COLORIZED_LOGS = !ENV['DISABLED_COLORIZED_LOGS'] && ENV.fetch('RACK_ENV', 'development') == 'development'
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_accessor :color_enabled, :structured
|
16
|
+
end
|
17
|
+
|
18
|
+
def structured
|
19
|
+
return @structured if defined?(@structured)
|
20
|
+
@structured = !LazyGraph::Environment.development?
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_logger
|
24
|
+
logger = ::Logger.new($stdout)
|
25
|
+
self.color_enabled ||= Logger::COLORIZED_LOGS
|
26
|
+
if self.color_enabled
|
27
|
+
logger.formatter = proc do |severity, datetime, progname, message|
|
28
|
+
light_gray_timestamp = "\e[90m[##{Process.pid}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%6N')}\e[0m" # Light gray timestamp
|
29
|
+
"#{light_gray_timestamp} \e[1m#{severity}\e[0m #{progname}: #{message}\n"
|
30
|
+
end
|
31
|
+
elsif self.structured
|
32
|
+
logger.formatter = proc do |severity, datetime, progname, message|
|
33
|
+
"#{{severity:, datetime:, progname:, **(message.is_a?(Hash) ? message : {message: }) }.to_json}\n"
|
34
|
+
end
|
35
|
+
else
|
36
|
+
logger.formatter = proc do |severity, datetime, progname, message|
|
37
|
+
"[##{Process.pid}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%6N')} #{severity} #{progname}: #{message}\n"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
logger
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_color_string(&blk)
|
44
|
+
return unless block_given?
|
45
|
+
|
46
|
+
instance_eval(&blk)
|
47
|
+
end
|
48
|
+
|
49
|
+
def colorize(text, color_code)
|
50
|
+
@color_enabled ? "\e[#{color_code}m#{text}\e[0m" : text
|
51
|
+
end
|
52
|
+
|
53
|
+
def green(text)
|
54
|
+
colorize(text, 32) # Green for success
|
55
|
+
end
|
56
|
+
|
57
|
+
def red(text)
|
58
|
+
colorize(text, 31) # Red for errors
|
59
|
+
end
|
60
|
+
|
61
|
+
def yellow(text)
|
62
|
+
colorize(text, 33) # Yellow for warnings or debug
|
63
|
+
end
|
64
|
+
|
65
|
+
def blue(text)
|
66
|
+
colorize(text, 34) # Blue for info
|
67
|
+
end
|
68
|
+
|
69
|
+
def light_gray(text)
|
70
|
+
colorize(text, 90) # Light gray for faded text
|
71
|
+
end
|
72
|
+
|
73
|
+
def orange(text)
|
74
|
+
colorize(text, '38;5;214')
|
75
|
+
end
|
76
|
+
|
77
|
+
def bold(text)
|
78
|
+
colorize(text, 1) # Bold text
|
79
|
+
end
|
80
|
+
|
81
|
+
def dim(text)
|
82
|
+
colorize(text, 2) # Italic text
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
self.logger = Logger.default_logger
|
87
|
+
end
|
@@ -8,13 +8,20 @@ module LazyGraph
|
|
8
8
|
|
9
9
|
def initialize(details) = @details = details
|
10
10
|
def to_s = "MISSING[#{@details}]"
|
11
|
-
|
12
11
|
def inspect = to_s
|
13
12
|
def coerce(other) = [self, other]
|
14
13
|
def as_json = nil
|
14
|
+
def to_h = nil
|
15
15
|
def +(other) = other
|
16
|
-
|
17
16
|
def respond_to_missing?(_method_name, _include_private = false) = true
|
17
|
+
def to_i = 0
|
18
|
+
def to_f = 0.0
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
return true if other.nil?
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
18
25
|
|
19
26
|
def method_missing(method, *args, &block)
|
20
27
|
return super if method == :to_ary
|
@@ -14,42 +14,38 @@ module LazyGraph
|
|
14
14
|
should_recycle = stack_memory,
|
15
15
|
**
|
16
16
|
)
|
17
|
-
input = stack_memory.frame
|
17
|
+
return MissingValue() unless input = stack_memory.frame
|
18
|
+
|
18
19
|
@visited[input.object_id >> 2 ^ path.shifted_id] ||= begin
|
20
|
+
path_next = path.next
|
19
21
|
if (path_segment = path.segment).is_a?(PathParser::PathGroup)
|
20
22
|
unless path_segment.index?
|
21
23
|
return input.length.times.map do |index|
|
22
|
-
|
23
|
-
children.resolve(path, stack_memory.push(item, index))
|
24
|
+
children.fetch_and_resolve(path, input, index, stack_memory)
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
27
|
-
return resolve(path_segment.options.first.merge(
|
28
|
+
return resolve(path_segment.options.first.merge(path_next), stack_memory, nil) if path_segment.options.one?
|
28
29
|
|
29
|
-
return path_segment.options.map { |part| resolve(part.merge(
|
30
|
+
return path_segment.options.map { |part| resolve(part.merge(path_next), stack_memory, nil) }
|
30
31
|
end
|
31
32
|
|
32
33
|
segment = path_segment&.part
|
33
34
|
case segment
|
34
35
|
when nil
|
35
|
-
|
36
|
-
|
37
|
-
|
36
|
+
|
37
|
+
unless @children.simple?
|
38
|
+
input.length.times do |index|
|
39
|
+
@children.fetch_and_resolve(path, input, index, stack_memory)
|
40
|
+
end
|
38
41
|
end
|
39
42
|
input
|
40
43
|
when DIGIT_REGEXP
|
41
|
-
|
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
|
44
|
+
@children.fetch_and_resolve(path_next, input, segment.to_s.to_i, stack_memory)
|
48
45
|
else
|
49
46
|
if @child_properties&.key?(segment) || input&.first&.key?(segment)
|
50
47
|
input.length.times.map do |index|
|
51
|
-
|
52
|
-
@children.resolve(path, stack_memory.push(item, index))
|
48
|
+
@children.fetch_and_resolve(path, input, index, stack_memory)
|
53
49
|
end
|
54
50
|
else
|
55
51
|
MissingValue()
|
@@ -66,7 +62,7 @@ module LazyGraph
|
|
66
62
|
end
|
67
63
|
|
68
64
|
def cast(value)
|
69
|
-
value
|
65
|
+
Array(value)
|
70
66
|
end
|
71
67
|
end
|
72
68
|
end
|
@@ -25,6 +25,7 @@ module LazyGraph
|
|
25
25
|
def build_derived_inputs(derived, helpers)
|
26
26
|
@resolvers = {}.compare_by_identity
|
27
27
|
@path_cache = {}.compare_by_identity
|
28
|
+
@resolution_stack = {}.compare_by_identity
|
28
29
|
|
29
30
|
derived = interpret_derived_proc(derived) if derived.is_a?(Proc)
|
30
31
|
derived = { inputs: derived.to_s } if derived.is_a?(String) || derived.is_a?(Symbol)
|
@@ -43,9 +44,10 @@ module LazyGraph
|
|
43
44
|
end
|
44
45
|
|
45
46
|
def interpret_derived_proc(derived)
|
46
|
-
src, requireds, optionals, keywords,
|
47
|
-
|
48
|
-
@src =
|
47
|
+
src, requireds, optionals, keywords, loc = DerivedRules.extract_expr_from_source_location(derived.source_location)
|
48
|
+
body = src.body&.slice || ''
|
49
|
+
@src = body.lines.map(&:strip)
|
50
|
+
offset = src.slice.lines.length - body.lines.length
|
49
51
|
inputs, conditions = parse_args_with_conditions(requireds, optionals, keywords)
|
50
52
|
|
51
53
|
{
|
@@ -53,11 +55,11 @@ module LazyGraph
|
|
53
55
|
mtime: File.mtime(derived.source_location.first),
|
54
56
|
conditions: conditions,
|
55
57
|
calc: instance_eval(
|
56
|
-
"->(#{inputs.keys.map { |k| "#{k}=self.#{k}" }.join(', ')}){ #{
|
58
|
+
"->(#{inputs.keys.map { |k| "#{k}=self.#{k}" }.join(', ')}){ #{body}}",
|
57
59
|
# rubocop:disable:next-line
|
58
60
|
derived.source_location.first,
|
59
61
|
# rubocop:enable
|
60
|
-
derived.source_location.last
|
62
|
+
derived.source_location.last + offset
|
61
63
|
)
|
62
64
|
}
|
63
65
|
end
|
@@ -77,13 +79,21 @@ module LazyGraph
|
|
77
79
|
[keywords, conditions.any? ? conditions : nil]
|
78
80
|
end
|
79
81
|
|
82
|
+
def self.get_file_body(file_path)
|
83
|
+
@file_body_cache ||= {}
|
84
|
+
if @file_body_cache[file_path]&.last.to_i < File.mtime(file_path).to_i
|
85
|
+
@file_body_cache[file_path] = [IO.readlines(file_path), File.mtime(file_path).to_i]
|
86
|
+
end
|
87
|
+
@file_body_cache[file_path]&.first
|
88
|
+
end
|
89
|
+
|
80
90
|
def self.extract_expr_from_source_location(source_location)
|
81
91
|
@derived_proc_cache ||= {}
|
82
92
|
mtime = File.mtime(source_location.first).to_i
|
83
|
-
|
84
93
|
if @derived_proc_cache[source_location]&.last.to_i.< mtime
|
85
94
|
@derived_proc_cache[source_location] = begin
|
86
|
-
source_lines =
|
95
|
+
source_lines = get_file_body(source_location.first)
|
96
|
+
|
87
97
|
proc_line = source_location.last - 1
|
88
98
|
first_line = source_lines[proc_line]
|
89
99
|
until first_line =~ /(?:lambda|proc|->)/ || proc_line.zero?
|
@@ -124,7 +134,7 @@ module LazyGraph
|
|
124
134
|
@derived_proc_cache[source_location]
|
125
135
|
rescue StandardError => e
|
126
136
|
LazyGraph.logger.error(e.message)
|
127
|
-
LazyGraph.logger.error(e.backtrace)
|
137
|
+
LazyGraph.logger.error(e.backtrace.join("\n"))
|
128
138
|
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"
|
129
139
|
end
|
130
140
|
|
@@ -132,15 +142,16 @@ module LazyGraph
|
|
132
142
|
inputs = derived[:inputs]
|
133
143
|
case inputs
|
134
144
|
when Symbol, String
|
135
|
-
if
|
145
|
+
if !derived[:calc]
|
136
146
|
@src ||= inputs
|
137
147
|
input_hash = {}
|
138
148
|
@input_mapper = {}
|
139
|
-
|
149
|
+
calc = inputs.gsub(PLACEHOLDER_VAR_REGEX) do |match|
|
140
150
|
sub = input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}"
|
141
151
|
@input_mapper[sub.to_sym] = match[2...-1].to_sym
|
142
152
|
sub
|
143
153
|
end
|
154
|
+
derived[:calc] = calc unless calc == input_hash.values.first
|
144
155
|
input_hash.invert
|
145
156
|
else
|
146
157
|
{ inputs.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => inputs.to_s.freeze }
|
@@ -167,10 +178,20 @@ module LazyGraph
|
|
167
178
|
|
168
179
|
def parse_rule_string(derived)
|
169
180
|
calc_str = derived[:calc]
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
181
|
+
node_path = path
|
182
|
+
|
183
|
+
src = <<~RUBY, @rule_location&.first, @rule_location&.last.to_i - 2
|
184
|
+
->{
|
185
|
+
begin
|
186
|
+
#{calc_str}
|
187
|
+
rescue StandardError => e;
|
188
|
+
LazyGraph.logger.error("Exception in \#{calc_str} => \#{node_path}. \#{e.message}")
|
189
|
+
raise e
|
190
|
+
end
|
191
|
+
}
|
192
|
+
RUBY
|
193
|
+
|
194
|
+
instance_eval(*src)
|
174
195
|
rescue SyntaxError
|
175
196
|
missing_value = MissingValue { "Syntax error in #{derived[:src]}" }
|
176
197
|
-> { missing_value }
|
@@ -194,12 +215,46 @@ module LazyGraph
|
|
194
215
|
end.new
|
195
216
|
end
|
196
217
|
|
218
|
+
def resolver_for(path)
|
219
|
+
segment = path.segment.part
|
220
|
+
return root.properties[path.next.segment.part] if segment == :'$'
|
221
|
+
|
222
|
+
(segment == name ? parent.parent : @parent).find_resolver_for(segment)
|
223
|
+
end
|
224
|
+
|
225
|
+
def rule_definition_backtrace
|
226
|
+
if @rule_location && @rule_location.size >= 2
|
227
|
+
rule_file, rule_line = @rule_location
|
228
|
+
rule_entry = "#{rule_file}:#{rule_line}:in `rule`"
|
229
|
+
else
|
230
|
+
rule_entry = 'unknown_rule_location'
|
231
|
+
end
|
232
|
+
|
233
|
+
current_backtrace = caller.reverse.take_while { |line| !line.include?('/lib/lazy_graph/') }.reverse
|
234
|
+
[rule_entry] + current_backtrace
|
235
|
+
end
|
236
|
+
|
197
237
|
def map_derived_inputs_to_paths(inputs)
|
198
238
|
inputs.values.map.with_index do |path, idx|
|
199
|
-
|
200
|
-
segment.is_a?(PathParser::PathGroup) &&
|
239
|
+
segments = path.parts.map.with_index do |segment, i|
|
240
|
+
if segment.is_a?(PathParser::PathGroup) &&
|
241
|
+
segment.options.length == 1 && !((resolver = resolver_for(segment.options.first)) || segment.options.first.segment.part.to_s =~ /\d+/)
|
242
|
+
raise(ValidationError.new(
|
243
|
+
"Invalid dependency in #{@path}: #{segment.options.first.to_path_str} cannot be resolved."
|
244
|
+
).tap { |e| e.set_backtrace(rule_definition_backtrace) })
|
245
|
+
end
|
246
|
+
|
247
|
+
resolver ? [i, resolver] : nil
|
201
248
|
end.compact
|
202
|
-
|
249
|
+
resolver = resolver_for(path)
|
250
|
+
|
251
|
+
unless resolver
|
252
|
+
raise(ValidationError.new(
|
253
|
+
"Invalid dependency in #{@path}: #{path.to_path_str} cannot be resolved."
|
254
|
+
).tap { |e| e.set_backtrace(rule_definition_backtrace) })
|
255
|
+
end
|
256
|
+
|
257
|
+
[path, resolver, idx, segments.any? ? segments : nil]
|
203
258
|
end
|
204
259
|
end
|
205
260
|
end
|
@@ -8,14 +8,27 @@ module LazyGraph
|
|
8
8
|
members.each { |k| self[k] = kws[k].then { |v| v.nil? ? MissingValue::BLANK : v } }
|
9
9
|
end
|
10
10
|
|
11
|
+
members.each do |m|
|
12
|
+
define_method(m) do
|
13
|
+
self[m]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
alias_method :original_get, :[]
|
18
|
+
|
11
19
|
define_method(:key?) do |x|
|
12
|
-
!
|
20
|
+
!original_get(x).equal?(MissingValue::BLANK)
|
13
21
|
end
|
14
22
|
|
15
23
|
define_method(:[]=) do |key, val|
|
16
24
|
super(key, val)
|
17
25
|
end
|
18
26
|
|
27
|
+
define_method(:[]) do |key|
|
28
|
+
res = original_get(key)
|
29
|
+
res.is_a?(MissingValue) ? nil : res
|
30
|
+
end
|
31
|
+
|
19
32
|
define_method(:members) do
|
20
33
|
members
|
21
34
|
end
|
@@ -28,6 +41,17 @@ module LazyGraph
|
|
28
41
|
to_h
|
29
42
|
end
|
30
43
|
|
44
|
+
def to_h
|
45
|
+
HashUtils.strip_missing(self)
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
return super if other.is_a?(self.class)
|
50
|
+
return to_h.eql?(other.to_h.keep_if { |_, v| !v.nil? }) if other.respond_to?(:to_h)
|
51
|
+
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
31
55
|
define_method(:each_key, &members.method(:each))
|
32
56
|
|
33
57
|
def dup
|
@@ -36,23 +60,24 @@ module LazyGraph
|
|
36
60
|
|
37
61
|
def get_first_of(*props)
|
38
62
|
key = props.find do |prop|
|
39
|
-
!
|
63
|
+
!original_get(prop).is_a?(MissingValue)
|
40
64
|
end
|
41
65
|
key ? self[key] : MissingValue::BLANK
|
42
66
|
end
|
43
67
|
|
44
68
|
def pretty_print(q)
|
45
|
-
q.group(1, '<
|
69
|
+
q.group(1, '<', '>') do
|
70
|
+
q.text "#{self.class.name} "
|
46
71
|
q.seplist(members.zip(values).reject do |m, v|
|
47
|
-
m == :DEBUG
|
48
|
-
end) do |
|
49
|
-
q.
|
50
|
-
|
51
|
-
value.respond_to?(:pretty_print) ? q.pp(value) : q.text(value.inspect)
|
52
|
-
end
|
72
|
+
m == :DEBUG || v.nil? || v.is_a?(MissingValue)
|
73
|
+
end.to_h) do |k, v|
|
74
|
+
q.text "#{k}: "
|
75
|
+
q.pp v
|
53
76
|
end
|
54
77
|
end
|
55
78
|
end
|
79
|
+
|
80
|
+
alias_method :keys, :members
|
56
81
|
end
|
57
82
|
end
|
58
83
|
end
|