lazy_graph 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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: []