lazy_graph 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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: []
|