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 +4 -4
- data/README.md +55 -14
- data/examples/performance_tests.rb +10 -18
- data/lib/lazy_graph/builder/dsl.rb +10 -10
- data/lib/lazy_graph/builder.rb +39 -7
- data/lib/lazy_graph/context.rb +6 -4
- data/lib/lazy_graph/graph.rb +22 -32
- data/lib/lazy_graph/hash_utils.rb +29 -37
- data/lib/lazy_graph/missing_value.rb +2 -0
- data/lib/lazy_graph/node/array_node.rb +9 -4
- data/lib/lazy_graph/node/derived_rules.rb +20 -9
- data/lib/lazy_graph/node/node_properties.rb +9 -12
- data/lib/lazy_graph/node/object_node.rb +25 -19
- data/lib/lazy_graph/node/symbol_hash.rb +26 -0
- data/lib/lazy_graph/node.rb +66 -60
- data/lib/lazy_graph/path_parser/path.rb +3 -2
- data/lib/lazy_graph/server.rb +7 -2
- data/lib/lazy_graph/stack_pointer.rb +21 -9
- data/lib/lazy_graph/version.rb +1 -1
- metadata +4 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29043baea3639e83c226d8e157af818466d50d4e3c125853dac64ae411aa0c0a
|
4
|
+
data.tar.gz: f137caa57317cd9046bc16384bf657ca7c43d10b12701f30be6bc7a31580c6db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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 :
|
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
|
-
|
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.
|
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
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
290
|
+
calc = "{#{when_clause.keys.map { |k| "#{k}: #{k}}" }.join(', ')}"
|
292
291
|
{
|
293
292
|
inputs: inputs,
|
294
293
|
conditions: conditions,
|
295
|
-
|
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
|
-
|
302
|
+
calc: "itself.get_first_of(:#{prop_list.join(', :')})"
|
303
303
|
}
|
304
304
|
end
|
305
305
|
|
data/lib/lazy_graph/builder.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
102
|
+
builder
|
82
103
|
end
|
83
104
|
|
84
|
-
|
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:
|
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
|
|
data/lib/lazy_graph/context.rb
CHANGED
@@ -7,23 +7,25 @@ module LazyGraph
|
|
7
7
|
attr_accessor :ruleset, :input
|
8
8
|
|
9
9
|
def initialize(graph, input)
|
10
|
-
HashUtils.
|
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(
|
17
|
+
paths.is_a?(Array) ? paths.map { |path| resolve(path) } : resolve(paths)
|
18
18
|
end
|
19
19
|
|
20
|
-
def resolve(
|
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]
|
data/lib/lazy_graph/graph.rb
CHANGED
@@ -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(
|
17
|
-
@json_schema = HashUtils.deep_dup
|
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
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
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
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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,
|
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
|
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("
|
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
|
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
|
@@ -15,9 +15,9 @@ module LazyGraph
|
|
15
15
|
**
|
16
16
|
)
|
17
17
|
input = stack_memory.frame
|
18
|
-
@visited[input.object_id ^
|
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.
|
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 @
|
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
|
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.
|
146
|
+
{ inputs.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => inputs.to_s.freeze }
|
142
147
|
end
|
143
148
|
when Array
|
144
|
-
|
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
|
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
|
-
|
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
|
-
|
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]
|
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
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
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 @
|
31
|
+
if @pattern_properties.any? && input.keys.length > @properties_a.length
|
27
32
|
input.each_key do |key|
|
28
|
-
node = !@properties[key] && @
|
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] || @
|
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 = @
|
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
|
-
|
98
|
-
|
99
|
-
|
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
|
103
|
-
if
|
104
|
-
|
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
|
data/lib/lazy_graph/node.rb
CHANGED
@@ -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]&.
|
77
|
-
|
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:
|
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 ^
|
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.
|
147
|
-
|
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,
|
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 =
|
272
|
-
allowed_value.is_a?(Array) ? allowed_value.include?(@node_context[field]) : allowed_value
|
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
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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 ||=
|
15
|
-
def identity = @identity ||= parts&.each_with_index&.reduce(0) { |acc, (p, i)| acc ^ (p.object_id) << (i *
|
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] ||= \
|
data/lib/lazy_graph/server.rb
CHANGED
@@ -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
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
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)
|
data/lib/lazy_graph/version.rb
CHANGED
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.
|
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-
|
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.
|
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: []
|