lazy_graph 0.1.0 → 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: df09bd7d0c4f8a82c1cdbaf387dfc6e18ec9f9b6f492a1aa17b2942f1e14a947
4
- data.tar.gz: c9887fff1a0eacb38b5ec426f13ce5a8fc23e7d6602a1876a398773628a3461e
3
+ metadata.gz: 29043baea3639e83c226d8e157af818466d50d4e3c125853dac64ae411aa0c0a
4
+ data.tar.gz: f137caa57317cd9046bc16384bf657ca7c43d10b12701f30be6bc7a31580c6db
5
5
  SHA512:
6
- metadata.gz: a90efb82622865574cffd51c996ceeaec5f8f29534da07869ef3c0bb2de3056031ec4c0357b41a3e4fe0390e4e8e364bd07b76f1073b08a8beb95124f7eda301
7
- data.tar.gz: 508a463c441ebd661871d8655e52dd230bbdc2e4458898a6399099dcc6ef70d90455042f5e960c471f64c77fb56b1456a2c8919b6f0ac28ba769b0a2374fb8a0
6
+ metadata.gz: b1c5107a005882da016caa1fd54afa5131f296637fd6a7b18810d3b2c1f18893ae11938e63fc42ceec05def4ceba76afbe016b152d1c107225af148b06f6db52
7
+ data.tar.gz: b5a2126ac97b38e1e0d4ce23a1d46859a89979b0b09a2d3ad3e3676b763d6277745310ee08a0c49fbbc85bcd930d246362bd79793899726c2f763b0aa1f6fe44
data/README.md CHANGED
@@ -273,12 +273,10 @@ module ShoppingCart
273
273
  rules_module :cart_base do
274
274
  object :cart do
275
275
  array :items, required: true do
276
- items do
277
- string :name, required: true
278
- number :price, default: 1.0
279
- number :quantity, default: 1
280
- number :total, rule: '${price} * ${quantity}'
281
- end
276
+ string :name, required: true
277
+ number :price, default: 1.0
278
+ number :quantity, default: 1
279
+ number :total, rule: '${price} * ${quantity}'
282
280
  end
283
281
 
284
282
  number :cart_total, rule: '${items.total}.sum'
@@ -301,6 +299,30 @@ context = cart_schema.context({
301
299
  puts context['cart.cart_total'] # => 21.0
302
300
  ```
303
301
 
302
+ We can create any number of modules within a builder, and then merge them together in any combination to create a final schema.
303
+ E.g.
304
+
305
+ ```ruby
306
+ # A second module that can be merged into the schema
307
+ rules_module :stock do
308
+ object :stock do
309
+ number :available, default: 100
310
+ number :reserved, default: 0
311
+ number :total, rule: '${available} - ${reserved}'
312
+ end
313
+ end
314
+ ```
315
+
316
+ You can then merge these, by chaining module builder calls
317
+
318
+ ```ruby
319
+ # Combine two modules
320
+ cart_schema = ShoppingCart::CartBuilder.cart_base.stock.build! # (also accepts optional args like :debug and :validate)
321
+
322
+ # Use just a single module
323
+ cart_schema = ShoppingCart::CartBuilder.cart_base.build!
324
+ ```
325
+
304
326
  ### Rules and Dependency Resolution
305
327
 
306
328
  Rules let you define logic for computing new values from existing ones. LazyGraph:
@@ -371,6 +393,14 @@ Just type a variable by name and it will automatically be recursively resolved t
371
393
  *However* it is essential that you explicitly define all inputs to the rule to ensure resolution is correct,
372
394
  as LazyGraph will not automatically resolve any variables that are dynamically accessed.
373
395
  This is advanced functionality, and should be used with caution. In general, it is best to define all input dependencies explicitly.
396
+ You can put a breakpoint inside a lambda rule to inspect the current scope and understand what is available to you.
397
+ Check out:
398
+
399
+ `stack_ptr` - The current stack pointer, which is the current node in the graph.
400
+ `stack_ptr.parent` - The parent node in the graph (you can traverse up the graph by following the parent pointers).
401
+ `stack_ptr.key` - The key in the parent node where the current node is stored (e.g. an index in an array or property name)
402
+ `stack_ptr.frame` - The current frame in the graph, which contains actual graph data.
403
+ `itself` - The current node in the graph (same as stack_ptr.frame)
374
404
 
375
405
  ### Debug Mode & Recursive Dependency Detection
376
406
 
@@ -406,6 +436,9 @@ puts result[:debug_trace]
406
436
  In cases where you accidentally create **circular dependencies**, LazyGraph will log warnings to the debug logs, and detect and break infinite loops
407
437
  in the dependency resolution, ensuring that the remainder of the graph is still computed correctly.
408
438
 
439
+ ### Conditional Sub-Graphs
440
+ [TODO]
441
+
409
442
  ### Advanced Path Syntax
410
443
 
411
444
  LazyGraph’s query engine supports a flexible path notation:
@@ -429,7 +462,6 @@ For situations where you want to serve rules over HTTP:
429
462
 
430
463
  A minimal example might look like:
431
464
 
432
- [TODO] Verify example.
433
465
  ```ruby
434
466
  require 'lazy_graph'
435
467
  require 'lazy_graph/server'
@@ -453,20 +485,29 @@ module CartAPI
453
485
  # - context: { cart: { items: [ { name: "Widget", price: 2.5, quantity: 4 } ] } } # The input data to the schema
454
486
  module Cart
455
487
  class V1 < LazyGraph::Builder
488
+
489
+ # A module that can be merged into the schema
456
490
  rules_module :cart_base do |foo:, bar:|
457
491
  object :cart do
458
492
  array :items, required: true do
459
- items do
460
- string :name, required: true
461
- number :price, default: 1.0
462
- number :quantity, default: 1
463
- number :total, rule: '${price} * ${quantity}'
464
- end
493
+ string :name, required: true
494
+ number :price, default: 1.0
495
+ number :quantity, default: 1
496
+ number :total, rule: '${price} * ${quantity}'
465
497
  end
466
498
 
467
499
  number :cart_total, rule: '${items.total}.sum'
468
500
  end
469
501
  end
502
+
503
+ # A second module that can be merged into the schema
504
+ rules_module :stock do
505
+ object :stock do
506
+ number :available, default: 100
507
+ number :reserved, default: 0
508
+ number :total, rule: '${available} - ${reserved}'
509
+ end
510
+ end
470
511
  end
471
512
  end
472
513
 
@@ -488,7 +529,7 @@ Then send requests like:
488
529
  curl -X POST http://localhost:9292/cart/v1 \
489
530
  -H 'Content-Type: application/json' \
490
531
  -d '{
491
- "modules": { "cart" : {} },
532
+ "modules": { "cart" : {}, "stock": {} },
492
533
  "query": "cart.cart_total",
493
534
  "context": {
494
535
  "cart": {
@@ -8,29 +8,21 @@ class PerformanceBuilder < LazyGraph::Builder
8
8
  integer :employees_count, rule: { inputs: 'employees', calc: 'employees.size' }
9
9
 
10
10
  array :employees, required: true do
11
- items do
12
- string :id
13
- array :positions, required: true, default: [] do
14
- items do
15
- string :pay_schedule_id, invisible: true
16
- string :position_id, invisible: true
11
+ string :id
12
+ array :positions, required: true, default: [] do
13
+ string :pay_schedule_id, invisible: true
14
+ string :position_id, invisible: true
17
15
 
18
- object :position, rule: :"${$.positions[position_id]}" do
19
- number :base_rate
20
- number :salary
21
- number :bonus, rule: :'${base_rate} * 0.1'
22
- end
23
- object :pay_schedule, rule: :'${pay_schedules[pay_schedule_id]}'
24
- number :base_rate, rule: :"${position.base_rate}"
25
- string :employee_id, rule: :id
26
- end
27
- end
16
+ object :position, rule: :"${$.positions[position_id]}"
17
+ object :pay_schedule, rule: :'${pay_schedules[pay_schedule_id]}'
18
+ number :pay_rate, rule: :"${position.pay_rate}"
19
+ string :employee_id, rule: :id
28
20
  end
29
21
  end
30
22
 
31
23
  object :positions do
32
24
  object :".*", pattern_property: true do
33
- number :base_rate
25
+ number :pay_rate
34
26
  number :salary, default: 100_000
35
27
  end
36
28
  end
@@ -64,7 +56,7 @@ def gen_employees(n, m = 10)
64
56
  end.to_h,
65
57
  positions: [*1..m].map do |i|
66
58
  [i, {
67
- base_rate: Random.rand(10..100)
59
+ pay_rate: Random.rand(10..100)
68
60
  }]
69
61
  end.to_h
70
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,13 +261,12 @@ module LazyGraph
261
261
  **(description ? { description: description } : {}),
262
262
  **(rule ? { rule: rule } : {}),
263
263
  **opts,
264
- items: {
265
- type: type,
266
- properties: {},
267
- additionalProperties: false
268
- }
264
+ items: { properties: {} }.tap do |items|
265
+ yields(items) do
266
+ send(type, :items, &blk)
267
+ end
268
+ end[:properties][:items]
269
269
  }
270
- yields(new_array, &blk)
271
270
  required(name) if required && default.nil? && rule.nil?
272
271
  pattern_property ? set_pattern_property(name, new_array) : set_property(name, new_array)
273
272
  end
@@ -288,18 +287,19 @@ module LazyGraph
288
287
  def rule_from_when(when_clause)
289
288
  inputs = when_clause.keys
290
289
  conditions = when_clause
291
- rule = "{#{when_clause.keys.map { |k| "#{k}: #{k}}" }.join(', ')}"
290
+ calc = "{#{when_clause.keys.map { |k| "#{k}: #{k}}" }.join(', ')}"
292
291
  {
293
292
  inputs: inputs,
294
293
  conditions: conditions,
295
- rule: rule
294
+ fixed_result: when_clause,
295
+ calc: calc
296
296
  }
297
297
  end
298
298
 
299
299
  def rule_from_first_of(prop_list)
300
300
  {
301
301
  inputs: prop_list,
302
- rule: "itself.get_first_of(:#{prop_list.join(', :')})"
302
+ calc: "itself.get_first_of(:#{prop_list.join(', :')})"
303
303
  }
304
304
  end
305
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.
@@ -27,6 +37,17 @@ module LazyGraph
27
37
  end
28
38
  end
29
39
 
40
+ # Helper for defining a new entity in the schema (just a shorthand for defining a new method for now)
41
+ def self.entity(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
49
+ end
50
+
30
51
  class << self
31
52
  attr_reader :helper_modules
32
53
  end
@@ -67,7 +88,7 @@ module LazyGraph
67
88
  end
68
89
 
69
90
  def self.eval!(modules:, context:, query:, debug: false, validate: false)
70
- context_result = (@eval_cache ||= {})[[modules, context, query, debug, validate].hash] ||= begin
91
+ builder = cache_as(:graph, [modules, debug, validate].hash) do
71
92
  invalid_modules = modules.reject { |k, _v| rules_modules[:properties].key?(k.to_sym) }
72
93
  return format_error_response('Invalid Modules', invalid_modules.keys.join(',')) unless invalid_modules.empty?
73
94
 
@@ -78,21 +99,24 @@ module LazyGraph
78
99
  builder = build_modules(modules)
79
100
  return builder if builder.is_a?(Hash)
80
101
 
81
- evaluate_context(builder, context, debug: debug, validate: validate)
102
+ builder
82
103
  end
83
104
 
84
- @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
85
108
 
86
109
  return context_result if context_result.is_a?(Hash) && context_result[:type] == :error
87
110
 
111
+ query_result = cache_as(:query, [context_result, query]){ context_result.query(*(query || '')) }
112
+
88
113
  {
89
114
  type: :success,
90
- result: context_result.query(*(query || ''))
115
+ result: query_result
91
116
  }
92
-
93
117
  rescue SystemStackError => e
94
118
  LazyGraph.logger.error(e.message)
95
- LazyGraph.logger.error(e.backtrace)
119
+ LazyGraph.logger.error(e.backtrace.join("\n"))
96
120
  {
97
121
  type: :error,
98
122
  message: 'Recursive Query Detected',
@@ -100,6 +124,14 @@ module LazyGraph
100
124
  }
101
125
  end
102
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
+
103
135
  private_class_method def self.method_missing(method_name, *args, &block) = new.send(method_name, *args, &block)
104
136
  private_class_method def self.respond_to_missing?(_, _ = false) = true
105
137
 
@@ -7,23 +7,25 @@ module LazyGraph
7
7
  attr_accessor :ruleset, :input
8
8
 
9
9
  def initialize(graph, input)
10
- HashUtils.deep_symbolize!(input)
10
+ input = HashUtils.deep_dup(input, symbolize: true)
11
11
  graph.validate!(input) if [true, 'input'].include?(graph.validate)
12
12
  @graph = graph
13
13
  @input = input
14
14
  end
15
15
 
16
16
  def query(paths)
17
- 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)
18
18
  end
19
19
 
20
- def resolve(input, path)
21
- @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
+
22
23
  query = PathParser.parse(path, true)
23
24
  stack = StackPointer.new(nil, @input, 0, :'$', nil)
24
25
  stack.root = stack
25
26
 
26
27
  result = @graph.root_node.resolve(query, stack)
28
+
27
29
  @graph.root_node.clear_visits!
28
30
  if @graph.debug?
29
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,
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,28 +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 = hash.dup
11
- hash.each do |key, value|
12
- 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)
13
22
  end
14
23
  when Array
15
- 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
16
30
  end
17
- hash
18
31
  end
19
32
 
20
- def deep_merge(hash, other_hash, path = :'')
33
+ def deep_merge(hash, other_hash, path = '')
21
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
+
22
37
  if this_val.is_a?(Hash) && other_val.is_a?(Hash) && other_val != this_val
23
- deep_merge(this_val, other_val, :"#{path}.#{key}")
38
+ deep_merge(this_val, other_val, current_path)
24
39
  elsif this_val.is_a?(Array) && other_val.is_a?(Array) && other_val != this_val
25
- this_val.concat(other_val).uniq
40
+ (this_val | other_val)
26
41
  else
27
42
  if this_val != other_val && !(this_val.is_a?(Proc) && other_val.is_a?(Proc))
28
- LazyGraph.logger.warn("Warning: Conflicting values at #{path}.#{key}. #{this_val} != #{other_val} ")
43
+ LazyGraph.logger.warn("Conflicting values at #{current_path}: #{this_val.inspect} != #{other_val.inspect}")
29
44
  end
30
45
  other_val
31
46
  end
@@ -51,37 +66,14 @@ module LazyGraph
51
66
  res[key] = strip_invalid(obj[key], parent_list)
52
67
  end
53
68
  when Array
54
- obj.map! { |value| strip_invalid(value, parent_list) }
69
+ obj.map { |value| strip_invalid(value, parent_list) }
70
+ when MissingValue
71
+ nil
55
72
  else
56
73
  obj
57
74
  end
58
75
  ensure
59
76
  parent_list.delete(obj) unless circular_dependency
60
77
  end
61
-
62
- def deep_symbolize!(obj)
63
- case obj
64
- when Hash
65
- hash = 0
66
- obj.to_a.each do |key, value|
67
- hash ^= deep_symbolize!(value)
68
- unless key.is_a?(Symbol)
69
- key.to_s.to_sym
70
- obj[key.to_s.to_sym] = obj.delete(key)
71
- end
72
- hash ^= key.object_id
73
- end
74
- obj.compare_by_identity
75
- hash
76
- when Array
77
- hash = 0
78
- obj.each { |item| hash ^= deep_symbolize!(item) }
79
- hash
80
- when String, Numeric, TrueClass, FalseClass, NilClass
81
- obj.hash
82
- else
83
- 0
84
- end
85
- end
86
78
  end
87
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.object_id << 8)] ||= 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
 
@@ -132,16 +133,21 @@ module LazyGraph
132
133
  case inputs
133
134
  when Symbol, String
134
135
  if inputs =~ PLACEHOLDER_VAR_REGEX && !derived[:calc]
136
+ @src ||= inputs
135
137
  input_hash = {}
138
+ @input_mapper = {}
136
139
  derived[:calc] = inputs.gsub(PLACEHOLDER_VAR_REGEX) do |match|
137
- input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}"
140
+ sub = input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}"
141
+ @input_mapper[sub.to_sym] = match[2...-1].to_sym
142
+ sub
138
143
  end
139
144
  input_hash.invert
140
145
  else
141
- { 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 }
142
147
  end
143
148
  when Array
144
- 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)
145
151
  when Hash
146
152
  inputs
147
153
  else
@@ -150,7 +156,7 @@ module LazyGraph
150
156
  end
151
157
 
152
158
  def extract_derived_src(derived)
153
- return @src = derived[:calc].to_s.lines unless derived[:calc].is_a?(Proc)
159
+ return @src ||= derived[:calc].to_s.lines unless derived[:calc].is_a?(Proc)
154
160
 
155
161
  @src ||= begin
156
162
  extract_expr_from_source_location(derived[:calc].source_location).body.slice.lines.map(&:strip)
@@ -160,7 +166,11 @@ module LazyGraph
160
166
  end
161
167
 
162
168
  def parse_rule_string(derived)
163
- 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
+ )
164
174
  rescue SyntaxError
165
175
  missing_value = MissingValue { "Syntax error in #{derived[:src]}" }
166
176
  -> { missing_value }
@@ -172,7 +182,8 @@ module LazyGraph
172
182
  Struct.new(*(derived[:inputs].keys.map(&:to_sym) + %i[itself stack_ptr])) do
173
183
  def missing?(value) = value.is_a?(LazyGraph::MissingValue) || value.nil?
174
184
  helpers&.each { |h| include h }
175
- define_method(:process!, &derived[:calc])
185
+
186
+ define_method(:process!, &derived[:calc]) if derived[:calc].is_a?(Proc)
176
187
  def method_missing(name, *args, &block)
177
188
  stack_ptr.send(name, *args, &block)
178
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.object_id << 8)] ||= 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,28 +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.default_proc = ->(h, k) { k.is_a?(Symbol) ? nil : h[k.to_s.to_sym] }
105
- value.compare_by_identity
106
- elsif @property_class && !value.is_a?(@property_class)
107
- @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) }
108
114
  else
109
- value
115
+ ->(value) { value }
110
116
  end
111
117
  end
112
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.object_id << 8)
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
@@ -240,7 +250,6 @@ module LazyGraph
240
250
  calc: @src
241
251
  )
242
252
  end
243
-
244
253
  input[key] = result.nil? ? MissingValue { key } : result
245
254
  end
246
255
 
@@ -255,12 +264,11 @@ module LazyGraph
255
264
  break missing_value = part if part.is_a?(MissingValue)
256
265
 
257
266
  part_sym = part.to_s.to_sym
258
- parts_identity ^= part_sym.object_id << index
267
+ parts_identity ^= part_sym.object_id << (index * 8)
259
268
  parts[index] = @path_cache[part_sym] ||= PathParser::PathPart.new(part: part_sym)
260
269
  end
261
270
  path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
262
271
  end
263
-
264
272
  result = missing_value || resolve_input(stack, path, key)
265
273
  @node_context[i] = result.is_a?(MissingValue) ? nil : result
266
274
  end
@@ -268,22 +276,24 @@ module LazyGraph
268
276
  @node_context[:itself] = input
269
277
  @node_context[:stack_ptr] = stack
270
278
 
271
- conditions_passed = !@conditions || @conditions.all? do |field, allowed_value|
272
- allowed_value.is_a?(Array) ? allowed_value.include?(@node_context[field]) : allowed_value == @node_context[field]
273
- 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)
274
282
 
275
283
  ex = nil
276
284
  result = \
277
285
  if conditions_passed
278
286
  output = begin
279
- cast(@node_context.process!)
287
+ cast(@fixed_result || @node_context.process!)
280
288
  rescue LazyGraph::AbortError => e
281
289
  raise e
282
290
  rescue StandardError => e
283
291
  ex = e
292
+ LazyGraph.logger.error(e)
293
+ LazyGraph.logger.error(e.backtrace.join("\n"))
284
294
  MissingValue { "#{key} raised exception: #{e.message}" }
285
295
  end
286
- output = output.dup if @has_properties
296
+
287
297
  input[key] = output.nil? ? MissingValue { key } : output
288
298
  else
289
299
  MissingValue { key }
@@ -292,22 +302,18 @@ module LazyGraph
292
302
  if @debug
293
303
  stack.log_debug(
294
304
  output: :"#{stack}.#{key}",
295
- result: result,
296
- inputs: @node_context.to_h.except(:itself, :stack_ptr),
305
+ result: HashUtils.deep_dup(result),
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,9 +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?
15
- def identity = @identity ||= parts&.each_with_index&.reduce(0) { |acc, (p, i)| acc ^ (p.object_id) << (i * 4) }
14
+ def index? = @index ||= !empty? && segment&.index?
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 << 28
17
18
 
18
19
  def merge(other)
19
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,13 +5,21 @@ module LazyGraph
5
5
  POINTER_POOL = []
6
6
 
7
7
  StackPointer = Struct.new(:parent, :frame, :depth, :key, :root) do
8
+ attr_accessor :pointer_cache
9
+
10
+ def shifted_id = @shifted_id ||= object_id << 28
11
+
8
12
  def push(frame, key)
9
- (POINTER_POOL.pop || StackPointer.new).tap do |pointer|
10
- pointer.parent = self
11
- pointer.frame = frame
12
- pointer.key = key
13
- pointer.depth = depth + 1
14
- 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)
15
23
  end
16
24
  end
17
25
 
@@ -21,9 +29,8 @@ module LazyGraph
21
29
  end
22
30
 
23
31
  def ptr_at(index)
24
- return self if depth == index
25
-
26
- parent&.ptr_at(index)
32
+ @pointer_cache ||= {}.compare_by_identity
33
+ @pointer_cache[index] ||= depth == index ? self : parent&.ptr_at(index)
27
34
  end
28
35
 
29
36
  def method_missing(name, *args, &block)
@@ -36,9 +43,14 @@ module LazyGraph
36
43
  end
37
44
  end
38
45
 
46
+ def index
47
+ key
48
+ end
49
+
39
50
  def log_debug(**log_item)
40
51
  root.frame[:DEBUG] = [] if !root.frame[:DEBUG] || root.frame[:DEBUG].is_a?(MissingValue)
41
52
  root.frame[:DEBUG] << { **log_item, location: to_s }
53
+ nil
42
54
  end
43
55
 
44
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.0'
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.0
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: []