lazy_graph 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e561f823062b3d3aedb3274a14d80e6bf1a17547bc3fbcb691cab37fc22ee014
4
- data.tar.gz: c353086cec79a6ceaf1c38db742f222474172be184307dab69f00c5f054eb081
3
+ metadata.gz: 29043baea3639e83c226d8e157af818466d50d4e3c125853dac64ae411aa0c0a
4
+ data.tar.gz: f137caa57317cd9046bc16384bf657ca7c43d10b12701f30be6bc7a31580c6db
5
5
  SHA512:
6
- metadata.gz: 2ed546ebffbcefd7d5ed3aa2651dec8d692cc8354e37f05ac2cf99f1e059417a603d22aa54e37ef1bdcd9ef0e498f30e4ef38337a2ec4dc412553a2e385f2bd9
7
- data.tar.gz: 67735a1964a486e136aeb03ce355fc1c0a873dca5a86a6a953cabbcaa33f49b3ca4ab6082dadef92157ac94eb78c8bfb37b40c9ab1f0354c39807c3d97032042
6
+ metadata.gz: b1c5107a005882da016caa1fd54afa5131f296637fd6a7b18810d3b2c1f18893ae11938e63fc42ceec05def4ceba76afbe016b152d1c107225af148b06f6db52
7
+ data.tar.gz: b5a2126ac97b38e1e0d4ce23a1d46859a89979b0b09a2d3ad3e3676b763d6277745310ee08a0c49fbbc85bcd930d246362bd79793899726c2f763b0aa1f6fe44
@@ -15,14 +15,14 @@ class PerformanceBuilder < LazyGraph::Builder
15
15
 
16
16
  object :position, rule: :"${$.positions[position_id]}"
17
17
  object :pay_schedule, rule: :'${pay_schedules[pay_schedule_id]}'
18
- number :base_rate, rule: :"${position.base_rate}"
18
+ number :pay_rate, rule: :"${position.pay_rate}"
19
19
  string :employee_id, rule: :id
20
20
  end
21
21
  end
22
22
 
23
23
  object :positions do
24
24
  object :".*", pattern_property: true do
25
- number :base_rate
25
+ number :pay_rate
26
26
  number :salary, default: 100_000
27
27
  end
28
28
  end
@@ -56,7 +56,7 @@ def gen_employees(n, m = 10)
56
56
  end.to_h,
57
57
  positions: [*1..m].map do |i|
58
58
  [i, {
59
- base_rate: Random.rand(10..100)
59
+ pay_rate: Random.rand(10..100)
60
60
  }]
61
61
  end.to_h
62
62
  }
@@ -39,7 +39,7 @@ module LazyGraph
39
39
  end
40
40
 
41
41
  def set_pattern_property(pattern, value)
42
- pattern = pattern.to_s
42
+ pattern = pattern.to_sym
43
43
  properties = schema[:patternProperties] ||= {}
44
44
  properties[pattern] = \
45
45
  if properties.key?(pattern) && %i[object array].include?(properties[pattern][:type])
@@ -261,16 +261,12 @@ module LazyGraph
261
261
  **(description ? { description: description } : {}),
262
262
  **(rule ? { rule: rule } : {}),
263
263
  **opts,
264
- items: {
265
- type: type,
266
- **(
267
- type == :object ? { properties: {}, additionalProperties: false } : {}
268
- )
269
- }
264
+ items: { properties: {} }.tap do |items|
265
+ yields(items) do
266
+ send(type, :items, &blk)
267
+ end
268
+ end[:properties][:items]
270
269
  }
271
- yields(new_array) do
272
- items(&blk)
273
- end
274
270
  required(name) if required && default.nil? && rule.nil?
275
271
  pattern_property ? set_pattern_property(name, new_array) : set_property(name, new_array)
276
272
  end
@@ -291,18 +287,19 @@ module LazyGraph
291
287
  def rule_from_when(when_clause)
292
288
  inputs = when_clause.keys
293
289
  conditions = when_clause
294
- rule = "{#{when_clause.keys.map { |k| "#{k}: #{k}}" }.join(', ')}"
290
+ calc = "{#{when_clause.keys.map { |k| "#{k}: #{k}}" }.join(', ')}"
295
291
  {
296
292
  inputs: inputs,
297
293
  conditions: conditions,
298
- rule: rule
294
+ fixed_result: when_clause,
295
+ calc: calc
299
296
  }
300
297
  end
301
298
 
302
299
  def rule_from_first_of(prop_list)
303
300
  {
304
301
  inputs: prop_list,
305
- rule: "itself.get_first_of(:#{prop_list.join(', :')})"
302
+ calc: "itself.get_first_of(:#{prop_list.join(', :')})"
306
303
  }
307
304
  end
308
305
 
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  # Subclass LazyGraph::Builder to create new builder classes
4
3
  # which can be used to easily build a rule-set to be used as a LazyGraph.
5
4
  #
@@ -7,6 +6,17 @@ require_relative 'builder/dsl'
7
6
 
8
7
  module LazyGraph
9
8
  class Builder
9
+
10
+ # Cache up to a fixed number of graphs, context and queries
11
+ BUILD_CACHE_CONFIG = {
12
+ # Store up to 1000 graphs
13
+ graph: {size: 1000, cache: {}},
14
+ # Store up to 5000 configs
15
+ context: {size: 5000, cache: {}},
16
+ # Store up to 5000 queries
17
+ query: {size: 5000, cache: {}}
18
+ }.compare_by_identity.freeze
19
+
10
20
  include DSL
11
21
  # This class is responsible for piece-wise building of rules,
12
22
  # as a combined schema definition.
@@ -29,7 +39,13 @@ module LazyGraph
29
39
 
30
40
  # Helper for defining a new entity in the schema (just a shorthand for defining a new method for now)
31
41
  def self.entity(name, &blk)
32
- define_method(name, &blk)
42
+ module_body_func_name = :"_#{name}"
43
+ define_method(module_body_func_name, &blk)
44
+ define_method(name) do |**args, &inner_blk|
45
+ send(module_body_func_name, **args)
46
+ inner_blk&.call
47
+ self
48
+ end
33
49
  end
34
50
 
35
51
  class << self
@@ -72,7 +88,7 @@ module LazyGraph
72
88
  end
73
89
 
74
90
  def self.eval!(modules:, context:, query:, debug: false, validate: false)
75
- context_result = (@eval_cache ||= {})[[modules, context, query, debug, validate].hash] ||= begin
91
+ builder = cache_as(:graph, [modules, debug, validate].hash) do
76
92
  invalid_modules = modules.reject { |k, _v| rules_modules[:properties].key?(k.to_sym) }
77
93
  return format_error_response('Invalid Modules', invalid_modules.keys.join(',')) unless invalid_modules.empty?
78
94
 
@@ -83,21 +99,24 @@ module LazyGraph
83
99
  builder = build_modules(modules)
84
100
  return builder if builder.is_a?(Hash)
85
101
 
86
- evaluate_context(builder, context, debug: debug, validate: validate)
102
+ builder
87
103
  end
88
104
 
89
- @eval_cache.delete(@eval_cache.keys.first) if @eval_cache.size > 1000
105
+ context_result = cache_as(:context, [builder, context]) do
106
+ evaluate_context(builder, context, debug: debug, validate: validate)
107
+ end
90
108
 
91
109
  return context_result if context_result.is_a?(Hash) && context_result[:type] == :error
92
110
 
111
+ query_result = cache_as(:query, [context_result, query]){ context_result.query(*(query || '')) }
112
+
93
113
  {
94
114
  type: :success,
95
- result: context_result.query(*(query || ''))
115
+ result: query_result
96
116
  }
97
-
98
117
  rescue SystemStackError => e
99
118
  LazyGraph.logger.error(e.message)
100
- LazyGraph.logger.error(e.backtrace)
119
+ LazyGraph.logger.error(e.backtrace.join("\n"))
101
120
  {
102
121
  type: :error,
103
122
  message: 'Recursive Query Detected',
@@ -105,6 +124,14 @@ module LazyGraph
105
124
  }
106
125
  end
107
126
 
127
+ def self.cache_as(type, key)
128
+ cache, max_size = BUILD_CACHE_CONFIG[type].values_at(:cache, :size)
129
+ key = key.hash
130
+ cache[key] = cache[key] ? cache.delete(key) : yield
131
+ ensure
132
+ cache.delete(cache.keys.first) while cache.size > max_size
133
+ end
134
+
108
135
  private_class_method def self.method_missing(method_name, *args, &block) = new.send(method_name, *args, &block)
109
136
  private_class_method def self.respond_to_missing?(_, _ = false) = true
110
137
 
@@ -7,24 +7,25 @@ module LazyGraph
7
7
  attr_accessor :ruleset, :input
8
8
 
9
9
  def initialize(graph, input)
10
- input = HashUtils.deep_dup(input)
11
- HashUtils.deep_symbolize!(input)
10
+ input = HashUtils.deep_dup(input, symbolize: true)
12
11
  graph.validate!(input) if [true, 'input'].include?(graph.validate)
13
12
  @graph = graph
14
13
  @input = input
15
14
  end
16
15
 
17
16
  def query(paths)
18
- paths.is_a?(Array) ? paths.map { |path| resolve(input, path) } : resolve(input, paths)
17
+ paths.is_a?(Array) ? paths.map { |path| resolve(path) } : resolve(paths)
19
18
  end
20
19
 
21
- def resolve(input, path)
22
- @input = @graph.root_node.fetch_item({ input: input }, :input, nil)
20
+ def resolve(path)
21
+ @input = @graph.root_node.fetch_item({ input: @input }, :input, nil)
22
+
23
23
  query = PathParser.parse(path, true)
24
24
  stack = StackPointer.new(nil, @input, 0, :'$', nil)
25
25
  stack.root = stack
26
26
 
27
27
  result = @graph.root_node.resolve(query, stack)
28
+
28
29
  @graph.root_node.clear_visits!
29
30
  if @graph.debug?
30
31
  debug_trace = stack.frame[:DEBUG]
@@ -13,18 +13,13 @@ module LazyGraph
13
13
  def context(input) = Context.new(self, input)
14
14
  def debug? = @debug
15
15
 
16
- def initialize(json_schema, debug: false, validate: true, helpers: nil)
17
- @json_schema = HashUtils.deep_dup(json_schema).merge(type: :object)
18
-
16
+ def initialize(input_schema, debug: false, validate: true, helpers: nil)
17
+ @json_schema = HashUtils.deep_dup(input_schema, symbolize: true, signature: signature = [0]).merge(type: :object)
19
18
  @debug = debug
20
19
  @validate = validate
21
20
  @helpers = helpers
22
21
 
23
- signature = HashUtils.deep_symbolize!(@json_schema)
24
- if [true, 'schema'].include?(validate)
25
- VALIDATION_CACHE[signature] ||= validate!(@json_schema, METASCHEMA)
26
- true
27
- end
22
+ VALIDATION_CACHE[signature[0]] ||= validate!(@json_schema, METASCHEMA) if [true, 'schema'].include?(validate)
28
23
 
29
24
  if @json_schema[:type].to_sym != :object || @json_schema[:properties].nil?
30
25
  raise ArgumentError, 'Root schema must be a non-empty object'
@@ -35,31 +30,26 @@ module LazyGraph
35
30
 
36
31
  def build_node(schema, path = :'$', name = :root, parent = nil)
37
32
  schema[:type] = schema[:type].to_sym
38
- case schema[:type]
39
- when :object then ObjectNode
40
- when :array then ArrayNode
41
- else Node
42
- end.new(name, path, schema, parent, debug: @debug, helpers: @helpers).tap do |node|
43
- node.children = build_children(node, schema, path)
33
+ node = case schema[:type]
34
+ when :object then ObjectNode
35
+ when :array then ArrayNode
36
+ else Node
37
+ end.new(name, path, schema, parent, debug: @debug, helpers: @helpers)
38
+
39
+ if node.type == :object
40
+ node.children = \
41
+ {
42
+ properties: schema.fetch(:properties, {}).map do |key, value|
43
+ [key, build_node(value, :"#{path}.#{key}", key, node)]
44
+ end.to_h.compare_by_identity,
45
+ pattern_properties: schema.fetch(:patternProperties, {}).map do |key, value|
46
+ [Regexp.new(key.to_s), build_node(value, :"#{path}.#{key}", :'<property>', node)]
47
+ end
48
+ }
49
+ elsif node.type == :array
50
+ node.children = build_node(schema.fetch(:items, {}), :"#{path}[]", :items, node)
44
51
  end
45
- end
46
-
47
- def build_children(node, schema, path)
48
- case node.type
49
- when :object then build_object_children(schema, path, node)
50
- when :array then build_node(schema.fetch(:items, {}), :"#{path}[]", :items, node)
51
- end
52
- end
53
-
54
- def build_object_children(schema, path, parent)
55
- {
56
- properties: schema.fetch(:properties, {}).map do |key, value|
57
- [key, build_node(value, "#{path}.#{key}", key, parent)]
58
- end.to_h.compare_by_identity,
59
- pattern_properties: schema.fetch(:patternProperties, {}).map do |key, value|
60
- [Regexp.new(key.to_s), build_node(value, :"#{path}.#{key}", :'<property>', parent)]
61
- end.to_h
62
- }
52
+ node
63
53
  end
64
54
 
65
55
  def validate!(input, schema = @json_schema)
@@ -4,25 +4,43 @@ module LazyGraph
4
4
  module HashUtils
5
5
  module_function
6
6
 
7
- def deep_dup(hash)
8
- case hash
7
+ # Deeply duplicates a nested hash or array, preserving object identity.
8
+ # Optionally symbolizes keys on the way, and/or generates a signature.
9
+ def deep_dup(obj, symbolize: false, signature: nil)
10
+ case obj
9
11
  when Hash
10
- hash.dup.each { |key, value| hash[key] = deep_dup(value) }
12
+ obj.each_with_object(symbolize ? {}.compare_by_identity : {}) do |(key, value), result|
13
+ key = \
14
+ if !symbolize || key.is_a?(Symbol)
15
+ key
16
+ else
17
+ key.is_a?(String) ? key.to_sym : key.to_s.to_sym
18
+ end
19
+
20
+ signature[0] ^= key.object_id if signature
21
+ result[key] = deep_dup(value, symbolize: symbolize, signature: signature)
22
+ end
11
23
  when Array
12
- hash.map { |value| deep_dup(value) }
24
+ obj.map { |value| deep_dup(value, symbolize: symbolize, signature: signature) }
25
+ when String, Numeric, TrueClass, FalseClass, NilClass
26
+ signature[0] ^= obj.hash if signature
27
+ obj
28
+ else
29
+ obj
13
30
  end
14
- hash
15
31
  end
16
32
 
17
- def deep_merge(hash, other_hash, path = :'')
33
+ def deep_merge(hash, other_hash, path = '')
18
34
  hash.merge(other_hash.transform_keys(&:to_sym)) do |key, this_val, other_val|
35
+ current_path = path.empty? ? key.to_s : "#{path}.#{key}"
36
+
19
37
  if this_val.is_a?(Hash) && other_val.is_a?(Hash) && other_val != this_val
20
- deep_merge(this_val, other_val, :"#{path}.#{key}")
38
+ deep_merge(this_val, other_val, current_path)
21
39
  elsif this_val.is_a?(Array) && other_val.is_a?(Array) && other_val != this_val
22
- this_val.concat(other_val).uniq
40
+ (this_val | other_val)
23
41
  else
24
42
  if this_val != other_val && !(this_val.is_a?(Proc) && other_val.is_a?(Proc))
25
- LazyGraph.logger.warn("Warning: Conflicting values at #{path}.#{key}. #{this_val} != #{other_val} ")
43
+ LazyGraph.logger.warn("Conflicting values at #{current_path}: #{this_val.inspect} != #{other_val.inspect}")
26
44
  end
27
45
  other_val
28
46
  end
@@ -48,37 +66,14 @@ module LazyGraph
48
66
  res[key] = strip_invalid(obj[key], parent_list)
49
67
  end
50
68
  when Array
51
- obj.map! { |value| strip_invalid(value, parent_list) }
69
+ obj.map { |value| strip_invalid(value, parent_list) }
70
+ when MissingValue
71
+ nil
52
72
  else
53
73
  obj
54
74
  end
55
75
  ensure
56
76
  parent_list.delete(obj) unless circular_dependency
57
77
  end
58
-
59
- def deep_symbolize!(obj)
60
- case obj
61
- when Hash
62
- hash = 0
63
- obj.to_a.each do |key, value|
64
- hash ^= deep_symbolize!(value)
65
- unless key.is_a?(Symbol)
66
- key.to_s.to_sym
67
- obj[key.to_s.to_sym] = obj.delete(key)
68
- end
69
- hash ^= key.object_id
70
- end
71
- obj.compare_by_identity
72
- hash
73
- when Array
74
- hash = 0
75
- obj.each { |item| hash ^= deep_symbolize!(item) }
76
- hash
77
- when String, Numeric, TrueClass, FalseClass, NilClass
78
- obj.hash
79
- else
80
- 0
81
- end
82
- end
83
78
  end
84
79
  end
@@ -12,6 +12,8 @@ module LazyGraph
12
12
  def inspect = to_s
13
13
  def coerce(other) = [self, other]
14
14
  def as_json = nil
15
+ def +(other) = other
16
+
15
17
  def respond_to_missing?(_method_name, _include_private = false) = true
16
18
 
17
19
  def method_missing(method, *args, &block)
@@ -15,9 +15,9 @@ module LazyGraph
15
15
  **
16
16
  )
17
17
  input = stack_memory.frame
18
- @visited[input.object_id ^ path.shifted_id] ||= begin
18
+ @visited[input.object_id >> 2 ^ path.shifted_id] ||= begin
19
19
  if (path_segment = path.segment).is_a?(PathParser::PathGroup)
20
- unless path_segment.options.all?(&:index?)
20
+ unless path_segment.index?
21
21
  return input.length.times.map do |index|
22
22
  item = children.fetch_item(input, index, stack_memory)
23
23
  children.resolve(path, stack_memory.push(item, index))
@@ -46,7 +46,7 @@ module LazyGraph
46
46
  @children.resolve(path.next, stack_memory.push(item, index))
47
47
  end
48
48
  else
49
- if @children.is_object && @children.children[:properties].keys.include?(segment) || input&.first&.key?(segment)
49
+ if @child_properties&.key?(segment) || input&.first&.key?(segment)
50
50
  input.length.times.map do |index|
51
51
  item = children.fetch_item(input, index, stack_memory)
52
52
  @children.resolve(path, stack_memory.push(item, index))
@@ -60,8 +60,13 @@ module LazyGraph
60
60
  should_recycle&.recycle!
61
61
  end
62
62
 
63
+ def children=(value)
64
+ @children = value
65
+ @child_properties = @children.children[:properties].compare_by_identity if @children.is_object
66
+ end
67
+
63
68
  def cast(value)
64
- value.dup
69
+ value
65
70
  end
66
71
  end
67
72
  end
@@ -29,6 +29,7 @@ module LazyGraph
29
29
  derived = interpret_derived_proc(derived) if derived.is_a?(Proc)
30
30
  derived = { inputs: derived.to_s } if derived.is_a?(String) || derived.is_a?(Symbol)
31
31
  derived[:inputs] = parse_derived_inputs(derived)
32
+ @fixed_result = derived[:fixed_result]
32
33
  @copy_input = true if !derived[:calc] && derived[:inputs].size == 1
33
34
  extract_derived_src(derived) if @debug
34
35
 
@@ -42,7 +43,7 @@ module LazyGraph
42
43
  end
43
44
 
44
45
  def interpret_derived_proc(derived)
45
- src, requireds, optionals, keywords, = DerivedRules.extract_expr_from_source_location(derived.source_location)
46
+ src, requireds, optionals, keywords, proc_line, = DerivedRules.extract_expr_from_source_location(derived.source_location)
46
47
  src = src.body&.slice || ''
47
48
  @src = src.lines.map(&:strip)
48
49
  inputs, conditions = parse_args_with_conditions(requireds, optionals, keywords)
@@ -56,7 +57,7 @@ module LazyGraph
56
57
  # rubocop:disable:next-line
57
58
  derived.source_location.first,
58
59
  # rubocop:enable
59
- derived.source_location.last.succ
60
+ derived.source_location.last.succ.succ
60
61
  )
61
62
  }
62
63
  end
@@ -116,7 +117,7 @@ module LazyGraph
116
117
  keywords = (src.parameters&.parameters&.keywords || []).map do |kw|
117
118
  [kw.name, kw.value.slice.gsub(/^_\./, '$.')]
118
119
  end.to_h
119
- [src, requireds, optionals, keywords, mtime]
120
+ [src, requireds, optionals, keywords, proc_line, mtime]
120
121
  end
121
122
  end
122
123
 
@@ -142,10 +143,11 @@ module LazyGraph
142
143
  end
143
144
  input_hash.invert
144
145
  else
145
- { inputs.to_str.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => inputs.to_str.freeze }
146
+ { inputs.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => inputs.to_s.freeze }
146
147
  end
147
148
  when Array
148
- inputs.map { |v| { v.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => v } }.reduce({}, :merge)
149
+ pairs = inputs.last.is_a?(Hash) ? inputs.pop : {}
150
+ inputs.map { |v| { v.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => v } }.reduce(pairs, :merge)
149
151
  when Hash
150
152
  inputs
151
153
  else
@@ -164,7 +166,11 @@ module LazyGraph
164
166
  end
165
167
 
166
168
  def parse_rule_string(derived)
167
- instance_eval("->{ #{derived[:calc]} }", __FILE__, __LINE__)
169
+ calc_str = derived[:calc]
170
+ src = @src
171
+ instance_eval(
172
+ "->{ 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__
173
+ )
168
174
  rescue SyntaxError
169
175
  missing_value = MissingValue { "Syntax error in #{derived[:src]}" }
170
176
  -> { missing_value }
@@ -176,7 +182,8 @@ module LazyGraph
176
182
  Struct.new(*(derived[:inputs].keys.map(&:to_sym) + %i[itself stack_ptr])) do
177
183
  def missing?(value) = value.is_a?(LazyGraph::MissingValue) || value.nil?
178
184
  helpers&.each { |h| include h }
179
- define_method(:process!, &derived[:calc])
185
+
186
+ define_method(:process!, &derived[:calc]) if derived[:calc].is_a?(Proc)
180
187
  def method_missing(name, *args, &block)
181
188
  stack_ptr.send(name, *args, &block)
182
189
  end
@@ -5,13 +5,11 @@ module LazyGraph
5
5
  def self.build(members:, invisible:)
6
6
  Struct.new(*members, keyword_init: true) do
7
7
  define_method(:initialize) do |kws|
8
- members.each { |k| self[k] = kws[k] || MissingValue::BLANK }
8
+ members.each { |k| self[k] = kws[k].then { |v| v.nil? ? MissingValue::BLANK : v } }
9
9
  end
10
10
 
11
11
  define_method(:key?) do |x|
12
12
  !self[x].equal?(MissingValue::BLANK)
13
- rescue StandardError
14
- nil
15
13
  end
16
14
 
17
15
  define_method(:[]=) do |key, val|
@@ -26,12 +24,16 @@ module LazyGraph
26
24
  invisible
27
25
  end
28
26
 
27
+ def to_hash
28
+ to_h
29
+ end
30
+
29
31
  define_method(:each_key, &members.method(:each))
30
32
 
31
33
  def dup
32
- self.class.new(members.map do |k|
33
- [k, self[k].dup]
34
- end.to_h)
34
+ duplicate = self.class.new
35
+ members.each { duplicate[_1] = self[_1].dup }
36
+ duplicate
35
37
  end
36
38
 
37
39
  def get_first_of(*props)
@@ -42,18 +44,13 @@ module LazyGraph
42
44
  end
43
45
 
44
46
  def pretty_print(q)
45
- # Start the custom pretty print
46
47
  q.group(1, '<Props ', '>') do
47
48
  q.seplist(members.zip(values).reject do |m, v|
48
49
  m == :DEBUG && (v.nil? || v.is_a?(MissingValue))
49
50
  end) do |member, value|
50
51
  q.group do
51
52
  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
53
+ value.respond_to?(:pretty_print) ? q.pp(value) : q.text(value.inspect)
57
54
  end
58
55
  end
59
56
  end
@@ -1,5 +1,7 @@
1
1
  module LazyGraph
2
2
  class ObjectNode < Node
3
+ require_relative 'symbol_hash'
4
+
3
5
  # An object supports the following types of path resolutions.
4
6
  # 1. Property name: obj.property => value
5
7
  # 2. Property name group: obj[property1, property2] => { property1: value1, property2: value2 }
@@ -11,7 +13,10 @@ module LazyGraph
11
13
  preserve_keys: false
12
14
  )
13
15
  input = stack_memory.frame
14
- @visited[input.object_id ^ path.shifted_id] ||= begin
16
+
17
+ @visited[input.object_id >> 2 ^ path.shifted_id] ||= begin
18
+ return input if input.is_a?(MissingValue)
19
+
15
20
  if (path_segment = path.segment).is_a?(PathParser::PathGroup)
16
21
  return path_segment.options.each_with_object({}.tap(&:compare_by_identity)) do |part, object|
17
22
  resolve(part.merge(path.next), stack_memory, nil, preserve_keys: object)
@@ -23,9 +28,9 @@ module LazyGraph
23
28
  item = node.fetch_item(input, key, stack_memory)
24
29
  node.resolve(path.next, stack_memory.push(item, key))
25
30
  end
26
- if @pattern_properties_a.any? && input.keys.length > @properties_a.length
31
+ if @pattern_properties.any? && input.keys.length > @properties_a.length
27
32
  input.each_key do |key|
28
- node = !@properties[key] && @pattern_properties_a.find { |pattern, _value| pattern.match?(key) }&.last
33
+ node = !@properties[key] && @pattern_properties.find { |(pattern, _value)| pattern.match?(key) }&.last
29
34
  item = node.fetch_item(input, key, stack_memory)
30
35
  node.resolve(path.next, stack_memory.push(item, key))
31
36
  end
@@ -40,14 +45,14 @@ module LazyGraph
40
45
  elsif segment == :*
41
46
  # rubocop:disable
42
47
  (input.keys | @properties_a.map(&:first)).each do |key|
43
- next unless (node = @properties[key] || @pattern_properties_a.find do |pattern, _value|
48
+ next unless (node = @properties[key] || @pattern_properties.find do |(pattern, _value)|
44
49
  pattern.match?(key)
45
50
  end&.last)
46
51
 
47
52
  item = node.fetch_item(input, key, stack_memory)
48
53
  preserve_keys[key] = node.resolve(path.next, stack_memory.push(item, key))
49
54
  end
50
- elsif (_, prop = @pattern_properties_a.find { |key, _val| key.match?(segment) })
55
+ elsif (_, prop = @pattern_properties.find { |(key, _val)| key.match?(segment) })
51
56
  item = prop.fetch_item(input, segment, stack_memory)
52
57
  value = prop.resolve(
53
58
  path.next, stack_memory.push(item, segment)
@@ -85,33 +90,29 @@ module LazyGraph
85
90
 
86
91
  @properties = @children.fetch(:properties, {})
87
92
  @properties.compare_by_identity
88
- @pattern_properties = @children.fetch(:pattern_properties, {})
93
+ @pattern_properties = @children.fetch(:pattern_properties, [])
89
94
 
90
95
  @properties_a = @properties.to_a
91
- @pattern_properties_a = @pattern_properties.to_a
92
96
 
93
97
  @has_properties = @properties.any? || @pattern_properties.any?
94
- return if @pattern_properties.any?
95
- return unless @properties.any?
96
98
 
97
- invisible = @properties.select { |_k, v| v.invisible }.map(&:first)
98
- @property_class = PROPERTY_CLASSES[{ members: @properties.keys + (@debug && !parent ? [:DEBUG] : []),
99
- invisible: invisible }]
99
+ return unless @properties.any? || @pattern_properties.any?
100
+
101
+ if @pattern_properties.any?
102
+ @property_class = SymbolHash
103
+ else
104
+ invisible = @properties.select { |_k, v| v.invisible }.map(&:first)
105
+ @property_class = PROPERTY_CLASSES[{ members: @properties.keys + (@debug && !parent ? [:DEBUG] : []),
106
+ invisible: invisible }]
107
+ end
108
+ define_singleton_method(:cast, build_caster)
100
109
  end
101
110
 
102
- def cast(value)
103
- if !@property_class && value.is_a?(Hash)
104
- value.define_singleton_method(:[]=) do |k, v|
105
- super(!k.is_a?(Symbol) ? k.to_s.to_sym : k, v)
106
- end
107
- value.define_singleton_method(:[]) do |k|
108
- super(!k.is_a?(Symbol) ? k.to_s.to_sym : k)
109
- end
110
- value.compare_by_identity
111
- elsif @property_class && !value.is_a?(@property_class)
112
- @property_class.new(value.to_h)
111
+ private def build_caster
112
+ if @property_class
113
+ ->(value) { value.is_a?(@property_class) ? value : @property_class.new(value.to_h) }
113
114
  else
114
- value
115
+ ->(value) { value }
115
116
  end
116
117
  end
117
118
  end
@@ -0,0 +1,26 @@
1
+ module LazyGraph
2
+ class ObjectNode < Node
3
+ class SymbolHash < ::Hash
4
+ def initialize(input_hash)
5
+ super
6
+ merge!(input_hash)
7
+ end
8
+
9
+ def []=(key, value)
10
+ case key
11
+ when Symbol then super(key, value)
12
+ when String then super(key.to_sym, value)
13
+ else super(key.to_s.to_sym, value)
14
+ end
15
+ end
16
+
17
+ def [](key)
18
+ case key
19
+ when Symbol then super(key)
20
+ when String then super(key.to_sym)
21
+ else super(key.to_s.to_sym)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,3 +1,4 @@
1
+ require 'debug'
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'bigdecimal/util'
@@ -48,16 +49,16 @@ module LazyGraph
48
49
  @depth = parent ? parent.depth + 1 : 0
49
50
  @root = parent ? parent.root : self
50
51
  @type = node[:type]
51
- @invisible = node[:invisible]
52
+ @invisible = debug ? false : node[:invisible]
52
53
  @visited = {}.compare_by_identity
53
-
54
+ instance_variable_set("@is_#{@type}", true)
55
+ define_singleton_method(:cast, build_caster)
54
56
  define_missing_value_proc!
55
57
 
56
58
  @has_default = node.key?(:default)
57
- @default = @has_default ? node[:default] : MissingValue { @name }
59
+ @default = @has_default ? cast(node[:default]) : MissingValue { @name }
58
60
  @resolution_stack = []
59
61
 
60
- instance_variable_set("@is_#{@type}", true)
61
62
  build_derived_inputs(node[:rule], helpers) if node[:rule]
62
63
  end
63
64
 
@@ -68,16 +69,46 @@ module LazyGraph
68
69
  )
69
70
  end
70
71
 
72
+ private def build_caster
73
+ if @is_decimal
74
+ ->(value) { value.is_a?(BigDecimal) ? value : value.to_d }
75
+ elsif @is_date
76
+ ->(value) { value.is_a?(String) ? Date.parse(value) : value }
77
+ elsif @is_boolean
78
+ lambda do |value|
79
+ if value.is_a?(TrueClass) || value.is_a?(FalseClass)
80
+ value
81
+ else
82
+ value.is_a?(MissingValue) ? false : !!value
83
+ end
84
+ end
85
+ elsif @is_timestamp
86
+ lambda do |value|
87
+ case value
88
+ when String
89
+ DateTime.parse(value).to_time
90
+ when Numeric
91
+ Time.at(value)
92
+ else
93
+ value
94
+ end
95
+ end
96
+ else
97
+ ->(value) { value }
98
+ end
99
+ end
100
+
71
101
  def clear_visits!
72
102
  @visited.clear
103
+ @resolution_stack.clear
104
+ @path_cache = {}.clear
105
+ @resolvers = {}.clear
106
+
73
107
  return unless @children
74
108
  return @children.clear_visits! if @children.is_a?(Node)
75
109
 
76
- @children[:properties]&.each do |_, node|
77
- node.clear_visits!
78
- end
79
-
80
- @children[:pattern_properties]&.each do |_, node|
110
+ @children[:properties]&.each_value(&:clear_visits!)
111
+ @children[:pattern_properties]&.each do |(_, node)|
81
112
  node.clear_visits!
82
113
  end
83
114
  end
@@ -101,7 +132,7 @@ module LazyGraph
101
132
  case input
102
133
  when Hash
103
134
  node = Node.new(key, "#{path}.#{key}", { type: :object }, self)
104
- node.children = { properties: {}, pattern_properties: {} }
135
+ node.children = { properties: {}, pattern_properties: [] }
105
136
  node
106
137
  when Array
107
138
  node = Node.new(key, :"#{path}.#{key}[]", { type: :array }, self)
@@ -111,7 +142,7 @@ module LazyGraph
111
142
  when Array then :array
112
143
  end
113
144
  node.children = Node.new(:items, :"#{path}.#{key}[].items", { type: child_type }, node)
114
- node.children.children = { properties: {}, pattern_properties: {} } if child_type == :object
145
+ node.children.children = { properties: {}, pattern_properties: [] } if child_type == :object
115
146
  node
116
147
  else
117
148
  Node.new(key, :"#{path}.#{key}", {}, self)
@@ -131,7 +162,7 @@ module LazyGraph
131
162
  end
132
163
 
133
164
  def resolve_input(stack_memory, path, key)
134
- input_id = key.object_id ^ stack_memory.shifted_id
165
+ input_id = key.object_id >> 2 ^ stack_memory.shifted_id
135
166
  if @resolution_stack.include?(input_id)
136
167
  if @debug
137
168
  stack_memory.log_debug(
@@ -143,8 +174,9 @@ module LazyGraph
143
174
  end
144
175
 
145
176
  @resolution_stack << (input_id)
146
- first_segment = path.parts.first.part
147
- resolver_node = @resolvers[first_segment] ||= (first_segment == key ? parent.parent : parent).find_resolver_for(first_segment)
177
+ first_segment = path.segment.part
178
+
179
+ resolver_node = @resolvers[first_segment] ||= (first_segment == key ? parent.parent : @parent).find_resolver_for(first_segment)
148
180
 
149
181
  if resolver_node
150
182
  input_frame_pointer = stack_memory.ptr_at(resolver_node.depth)
@@ -161,35 +193,13 @@ module LazyGraph
161
193
  end
162
194
 
163
195
  def ancestors
164
- @ancestors ||= [self, *(parent ? parent.ancestors : [])]
196
+ @ancestors ||= [self, *(@parent ? @parent.ancestors : [])]
165
197
  end
166
198
 
167
199
  def find_resolver_for(segment)
168
200
  segment == :'$' ? root : @parent&.find_resolver_for(segment)
169
201
  end
170
202
 
171
- def cast(value)
172
- if @is_decimal
173
- value.is_a?(BigDecimal) ? value : value.to_d
174
- elsif @is_date
175
- value.is_a?(String) ? Date.parse(value) : value
176
- elsif @is_boolean
177
- if value.is_a?(TrueClass) || value.is_a?(FalseClass)
178
- value
179
- else
180
- value.is_a?(MissingValue) ? false : !!value
181
- end
182
- elsif @is_timestamp
183
- case value
184
- when String then DateTime.parse(value).to_time
185
- when Numeric then Time.at(value)
186
- else value
187
- end
188
- else
189
- value
190
- end
191
- end
192
-
193
203
  def fetch_item(input, key, stack)
194
204
  return MissingValue { key } unless input
195
205
 
@@ -214,7 +224,7 @@ module LazyGraph
214
224
  end
215
225
  end
216
226
 
217
- def copy_item!(input, key, stack, (path, i, segment_indexes))
227
+ def copy_item!(input, key, stack, (path, _i, segment_indexes))
218
228
  if segment_indexes
219
229
  missing_value = nil
220
230
  parts = path.parts.dup
@@ -259,7 +269,6 @@ module LazyGraph
259
269
  end
260
270
  path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
261
271
  end
262
-
263
272
  result = missing_value || resolve_input(stack, path, key)
264
273
  @node_context[i] = result.is_a?(MissingValue) ? nil : result
265
274
  end
@@ -267,22 +276,23 @@ module LazyGraph
267
276
  @node_context[:itself] = input
268
277
  @node_context[:stack_ptr] = stack
269
278
 
270
- conditions_passed = !@conditions || @conditions.all? do |field, allowed_value|
271
- allowed_value.is_a?(Array) ? allowed_value.include?(@node_context[field]) : allowed_value == @node_context[field]
272
- end
279
+ conditions_passed = !(@conditions&.any? do |field, allowed_value|
280
+ allowed_value.is_a?(Array) ? !allowed_value.include?(@node_context[field]) : allowed_value != @node_context[field]
281
+ end)
273
282
 
274
283
  ex = nil
275
284
  result = \
276
285
  if conditions_passed
277
286
  output = begin
278
- cast(@node_context.process!)
287
+ cast(@fixed_result || @node_context.process!)
279
288
  rescue LazyGraph::AbortError => e
280
289
  raise e
281
290
  rescue StandardError => e
282
291
  ex = e
292
+ LazyGraph.logger.error(e)
293
+ LazyGraph.logger.error(e.backtrace.join("\n"))
283
294
  MissingValue { "#{key} raised exception: #{e.message}" }
284
295
  end
285
- output = output.dup if @has_properties
286
296
 
287
297
  input[key] = output.nil? ? MissingValue { key } : output
288
298
  else
@@ -292,22 +302,18 @@ module LazyGraph
292
302
  if @debug
293
303
  stack.log_debug(
294
304
  output: :"#{stack}.#{key}",
295
- result: result,
305
+ result: HashUtils.deep_dup(result),
296
306
  inputs: @node_context.to_h.except(:itself, :stack_ptr).transform_keys { |k| @input_mapper&.[](k) || k },
297
307
  calc: @src,
298
308
  **(@conditions ? { conditions: @conditions } : {}),
299
- **(
300
- if ex
301
- {
302
- exception: ex,
303
- backtrace: ex.backtrace.take_while do |line|
304
- !line.include?('lazy_graph/node.rb')
305
- end
306
- }
307
- else
308
- {}
309
- end
310
- )
309
+ **(if ex
310
+ { exception: ex, backtrace: ex.backtrace.take_while do |line|
311
+ !line.include?('lazy_graph/node.rb')
312
+ end }
313
+ else
314
+ {}
315
+ end
316
+ )
311
317
  )
312
318
  end
313
319
  result
@@ -11,10 +11,10 @@ module LazyGraph
11
11
  def next = @next ||= parts.length <= 1 ? Path::BLANK : Path.new(parts: parts[1..])
12
12
  def empty? = @empty ||= parts.empty?
13
13
  def segment = @segment ||= parts&.[](0)
14
- def index? = @index ||= parts.any? && parts.first.index?
14
+ def index? = @index ||= !empty? && segment&.index?
15
15
  def identity = @identity ||= parts&.each_with_index&.reduce(0) { |acc, (p, i)| acc ^ (p.object_id) << (i * 8) }
16
16
  def map(&block) = empty? ? self : Path.new(parts: parts.map(&block))
17
- def shifted_id = @shifted_id ||= object_id << 32
17
+ def shifted_id = @shifted_id ||= object_id << 28
18
18
 
19
19
  def merge(other)
20
20
  (@merged ||= {})[other] ||= \
@@ -11,6 +11,7 @@ module LazyGraph
11
11
 
12
12
  def call(env)
13
13
  # Rack environment contains request details
14
+ env[:X_REQUEST_TIME_START] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
14
15
  request = Rack::Request.new(env)
15
16
 
16
17
  unless (graph_module = @routes[request.path.to_sym])
@@ -73,13 +74,17 @@ module LazyGraph
73
74
  error!(request, 404, 'Not Found', details)
74
75
  end
75
76
 
77
+ def request_ms(request)
78
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - request.env[:X_REQUEST_TIME_START]) * 1000.0).round(3)
79
+ end
80
+
76
81
  def success!(request, result, status: 200)
77
- LazyGraph.logger.info("#{request.request_method}: #{request.path} => #{status}")
82
+ LazyGraph.logger.info("#{request.request_method}: #{request.path} => #{status} #{request_ms(request)}ms")
78
83
  [status, { 'Content-Type' => 'text/json' }, [result.to_json]]
79
84
  end
80
85
 
81
86
  def error!(request, status, message, details = '')
82
- LazyGraph.logger.info("#{request.request_method}: #{request.path} => #{status}")
87
+ LazyGraph.logger.info("#{request.request_method}: #{request.path} => #{status} #{request_ms(request)}ms")
83
88
  [status, { 'Content-Type' => 'text/json' }, [{ 'error': message, 'details': details }.to_json]]
84
89
  end
85
90
  end
@@ -5,15 +5,21 @@ module LazyGraph
5
5
  POINTER_POOL = []
6
6
 
7
7
  StackPointer = Struct.new(:parent, :frame, :depth, :key, :root) do
8
- def shifted_id = @shifted_id ||= object_id << 32
8
+ attr_accessor :pointer_cache
9
+
10
+ def shifted_id = @shifted_id ||= object_id << 28
9
11
 
10
12
  def push(frame, key)
11
- (POINTER_POOL.pop || StackPointer.new).tap do |pointer|
12
- pointer.parent = self
13
- pointer.frame = frame
14
- pointer.key = key
15
- pointer.depth = depth + 1
16
- pointer.root = root || self
13
+ if (ptr = POINTER_POOL.pop)
14
+ ptr.parent = self
15
+ ptr.parent = self
16
+ ptr.frame = frame
17
+ ptr.key = key
18
+ ptr.depth = depth + 1
19
+ ptr.pointer_cache&.clear
20
+ ptr
21
+ else
22
+ StackPointer.new(parent: self, frame: frame, key: key, depth: depth + 1, root: root || self)
17
23
  end
18
24
  end
19
25
 
@@ -23,9 +29,8 @@ module LazyGraph
23
29
  end
24
30
 
25
31
  def ptr_at(index)
26
- return self if depth == index
27
-
28
- parent&.ptr_at(index)
32
+ @pointer_cache ||= {}.compare_by_identity
33
+ @pointer_cache[index] ||= depth == index ? self : parent&.ptr_at(index)
29
34
  end
30
35
 
31
36
  def method_missing(name, *args, &block)
@@ -38,9 +43,14 @@ module LazyGraph
38
43
  end
39
44
  end
40
45
 
46
+ def index
47
+ key
48
+ end
49
+
41
50
  def log_debug(**log_item)
42
51
  root.frame[:DEBUG] = [] if !root.frame[:DEBUG] || root.frame[:DEBUG].is_a?(MissingValue)
43
52
  root.frame[:DEBUG] << { **log_item, location: to_s }
53
+ nil
44
54
  end
45
55
 
46
56
  def respond_to_missing?(name, include_private = false)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LazyGraph
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.2'
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lazy_graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-12-22 00:00:00.000000000 Z
10
+ date: 2024-12-25 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: json-schema
@@ -164,6 +163,7 @@ files:
164
163
  - lib/lazy_graph/node/derived_rules.rb
165
164
  - lib/lazy_graph/node/node_properties.rb
166
165
  - lib/lazy_graph/node/object_node.rb
166
+ - lib/lazy_graph/node/symbol_hash.rb
167
167
  - lib/lazy_graph/path_parser.rb
168
168
  - lib/lazy_graph/path_parser/path.rb
169
169
  - lib/lazy_graph/path_parser/path_group.rb
@@ -178,7 +178,6 @@ licenses:
178
178
  metadata:
179
179
  homepage_uri: https://github.com/wouterken/lazy_graph
180
180
  source_code_uri: https://github.com/wouterken/lazy_graph
181
- post_install_message:
182
181
  rdoc_options: []
183
182
  require_paths:
184
183
  - lib
@@ -193,8 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
193
192
  - !ruby/object:Gem::Version
194
193
  version: '0'
195
194
  requirements: []
196
- rubygems_version: 3.5.22
197
- signing_key:
195
+ rubygems_version: 3.6.2
198
196
  specification_version: 4
199
197
  summary: JSON Driven, Stateless Rules Engine
200
198
  test_files: []