lazy_graph 0.1.0 → 0.1.1
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 +8 -16
- data/lib/lazy_graph/builder/dsl.rb +6 -3
- data/lib/lazy_graph/builder.rb +5 -0
- data/lib/lazy_graph/context.rb +1 -0
- data/lib/lazy_graph/graph.rb +2 -2
- data/lib/lazy_graph/hash_utils.rb +3 -6
- data/lib/lazy_graph/node/array_node.rb +1 -1
- data/lib/lazy_graph/node/derived_rules.rb +6 -2
- data/lib/lazy_graph/node/object_node.rb +7 -2
- data/lib/lazy_graph/node.rb +4 -4
- data/lib/lazy_graph/path_parser/path.rb +2 -1
- data/lib/lazy_graph/stack_pointer.rb +2 -0
- data/lib/lazy_graph/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e561f823062b3d3aedb3274a14d80e6bf1a17547bc3fbcb691cab37fc22ee014
|
4
|
+
data.tar.gz: c353086cec79a6ceaf1c38db742f222474172be184307dab69f00c5f054eb081
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ed546ebffbcefd7d5ed3aa2651dec8d692cc8354e37f05ac2cf99f1e059417a603d22aa54e37ef1bdcd9ef0e498f30e4ef38337a2ec4dc412553a2e385f2bd9
|
7
|
+
data.tar.gz: 67735a1964a486e136aeb03ce355fc1c0a873dca5a86a6a953cabbcaa33f49b3ca4ab6082dadef92157ac94eb78c8bfb37b40c9ab1f0354c39807c3d97032042
|
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,23 +8,15 @@ 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 :base_rate, rule: :"${position.base_rate}"
|
19
|
+
string :employee_id, rule: :id
|
28
20
|
end
|
29
21
|
end
|
30
22
|
|
@@ -263,11 +263,14 @@ module LazyGraph
|
|
263
263
|
**opts,
|
264
264
|
items: {
|
265
265
|
type: type,
|
266
|
-
|
267
|
-
|
266
|
+
**(
|
267
|
+
type == :object ? { properties: {}, additionalProperties: false } : {}
|
268
|
+
)
|
268
269
|
}
|
269
270
|
}
|
270
|
-
yields(new_array
|
271
|
+
yields(new_array) do
|
272
|
+
items(&blk)
|
273
|
+
end
|
271
274
|
required(name) if required && default.nil? && rule.nil?
|
272
275
|
pattern_property ? set_pattern_property(name, new_array) : set_property(name, new_array)
|
273
276
|
end
|
data/lib/lazy_graph/builder.rb
CHANGED
@@ -27,6 +27,11 @@ module LazyGraph
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
+
# Helper for defining a new entity in the schema (just a shorthand for defining a new method for now)
|
31
|
+
def self.entity(name, &blk)
|
32
|
+
define_method(name, &blk)
|
33
|
+
end
|
34
|
+
|
30
35
|
class << self
|
31
36
|
attr_reader :helper_modules
|
32
37
|
end
|
data/lib/lazy_graph/context.rb
CHANGED
data/lib/lazy_graph/graph.rb
CHANGED
@@ -14,7 +14,7 @@ module LazyGraph
|
|
14
14
|
def debug? = @debug
|
15
15
|
|
16
16
|
def initialize(json_schema, debug: false, validate: true, helpers: nil)
|
17
|
-
@json_schema = HashUtils.deep_dup
|
17
|
+
@json_schema = HashUtils.deep_dup(json_schema).merge(type: :object)
|
18
18
|
|
19
19
|
@debug = debug
|
20
20
|
@validate = validate
|
@@ -55,7 +55,7 @@ module LazyGraph
|
|
55
55
|
{
|
56
56
|
properties: schema.fetch(:properties, {}).map do |key, value|
|
57
57
|
[key, build_node(value, "#{path}.#{key}", key, parent)]
|
58
|
-
end.to_h,
|
58
|
+
end.to_h.compare_by_identity,
|
59
59
|
pattern_properties: schema.fetch(:patternProperties, {}).map do |key, value|
|
60
60
|
[Regexp.new(key.to_s), build_node(value, :"#{path}.#{key}", :'<property>', parent)]
|
61
61
|
end.to_h
|
@@ -4,15 +4,12 @@ module LazyGraph
|
|
4
4
|
module HashUtils
|
5
5
|
module_function
|
6
6
|
|
7
|
-
def deep_dup
|
7
|
+
def deep_dup(hash)
|
8
8
|
case hash
|
9
9
|
when Hash
|
10
|
-
hash =
|
11
|
-
hash.each do |key, value|
|
12
|
-
hash[key] = deep_dup!(value)
|
13
|
-
end
|
10
|
+
hash.dup.each { |key, value| hash[key] = deep_dup(value) }
|
14
11
|
when Array
|
15
|
-
hash.map { |value| deep_dup
|
12
|
+
hash.map { |value| deep_dup(value) }
|
16
13
|
end
|
17
14
|
hash
|
18
15
|
end
|
@@ -15,7 +15,7 @@ module LazyGraph
|
|
15
15
|
**
|
16
16
|
)
|
17
17
|
input = stack_memory.frame
|
18
|
-
@visited[input.object_id ^
|
18
|
+
@visited[input.object_id ^ path.shifted_id] ||= begin
|
19
19
|
if (path_segment = path.segment).is_a?(PathParser::PathGroup)
|
20
20
|
unless path_segment.options.all?(&:index?)
|
21
21
|
return input.length.times.map do |index|
|
@@ -132,9 +132,13 @@ module LazyGraph
|
|
132
132
|
case inputs
|
133
133
|
when Symbol, String
|
134
134
|
if inputs =~ PLACEHOLDER_VAR_REGEX && !derived[:calc]
|
135
|
+
@src ||= inputs
|
135
136
|
input_hash = {}
|
137
|
+
@input_mapper = {}
|
136
138
|
derived[:calc] = inputs.gsub(PLACEHOLDER_VAR_REGEX) do |match|
|
137
|
-
input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}"
|
139
|
+
sub = input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}"
|
140
|
+
@input_mapper[sub.to_sym] = match[2...-1].to_sym
|
141
|
+
sub
|
138
142
|
end
|
139
143
|
input_hash.invert
|
140
144
|
else
|
@@ -150,7 +154,7 @@ module LazyGraph
|
|
150
154
|
end
|
151
155
|
|
152
156
|
def extract_derived_src(derived)
|
153
|
-
return @src
|
157
|
+
return @src ||= derived[:calc].to_s.lines unless derived[:calc].is_a?(Proc)
|
154
158
|
|
155
159
|
@src ||= begin
|
156
160
|
extract_expr_from_source_location(derived[:calc].source_location).body.slice.lines.map(&:strip)
|
@@ -11,7 +11,7 @@ module LazyGraph
|
|
11
11
|
preserve_keys: false
|
12
12
|
)
|
13
13
|
input = stack_memory.frame
|
14
|
-
@visited[input.object_id ^
|
14
|
+
@visited[input.object_id ^ path.shifted_id] ||= begin
|
15
15
|
if (path_segment = path.segment).is_a?(PathParser::PathGroup)
|
16
16
|
return path_segment.options.each_with_object({}.tap(&:compare_by_identity)) do |part, object|
|
17
17
|
resolve(part.merge(path.next), stack_memory, nil, preserve_keys: object)
|
@@ -101,7 +101,12 @@ module LazyGraph
|
|
101
101
|
|
102
102
|
def cast(value)
|
103
103
|
if !@property_class && value.is_a?(Hash)
|
104
|
-
value.
|
104
|
+
value.define_singleton_method(:[]=) do |k, v|
|
105
|
+
super(!k.is_a?(Symbol) ? k.to_s.to_sym : k, v)
|
106
|
+
end
|
107
|
+
value.define_singleton_method(:[]) do |k|
|
108
|
+
super(!k.is_a?(Symbol) ? k.to_s.to_sym : k)
|
109
|
+
end
|
105
110
|
value.compare_by_identity
|
106
111
|
elsif @property_class && !value.is_a?(@property_class)
|
107
112
|
@property_class.new(value.to_h)
|
data/lib/lazy_graph/node.rb
CHANGED
@@ -131,7 +131,7 @@ module LazyGraph
|
|
131
131
|
end
|
132
132
|
|
133
133
|
def resolve_input(stack_memory, path, key)
|
134
|
-
input_id = key.object_id ^
|
134
|
+
input_id = key.object_id ^ stack_memory.shifted_id
|
135
135
|
if @resolution_stack.include?(input_id)
|
136
136
|
if @debug
|
137
137
|
stack_memory.log_debug(
|
@@ -240,7 +240,6 @@ module LazyGraph
|
|
240
240
|
calc: @src
|
241
241
|
)
|
242
242
|
end
|
243
|
-
|
244
243
|
input[key] = result.nil? ? MissingValue { key } : result
|
245
244
|
end
|
246
245
|
|
@@ -255,7 +254,7 @@ module LazyGraph
|
|
255
254
|
break missing_value = part if part.is_a?(MissingValue)
|
256
255
|
|
257
256
|
part_sym = part.to_s.to_sym
|
258
|
-
parts_identity ^= part_sym.object_id << index
|
257
|
+
parts_identity ^= part_sym.object_id << (index * 8)
|
259
258
|
parts[index] = @path_cache[part_sym] ||= PathParser::PathPart.new(part: part_sym)
|
260
259
|
end
|
261
260
|
path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
|
@@ -284,6 +283,7 @@ module LazyGraph
|
|
284
283
|
MissingValue { "#{key} raised exception: #{e.message}" }
|
285
284
|
end
|
286
285
|
output = output.dup if @has_properties
|
286
|
+
|
287
287
|
input[key] = output.nil? ? MissingValue { key } : output
|
288
288
|
else
|
289
289
|
MissingValue { key }
|
@@ -293,7 +293,7 @@ module LazyGraph
|
|
293
293
|
stack.log_debug(
|
294
294
|
output: :"#{stack}.#{key}",
|
295
295
|
result: result,
|
296
|
-
inputs: @node_context.to_h.except(:itself, :stack_ptr),
|
296
|
+
inputs: @node_context.to_h.except(:itself, :stack_ptr).transform_keys { |k| @input_mapper&.[](k) || k },
|
297
297
|
calc: @src,
|
298
298
|
**(@conditions ? { conditions: @conditions } : {}),
|
299
299
|
**(
|
@@ -12,8 +12,9 @@ module LazyGraph
|
|
12
12
|
def empty? = @empty ||= parts.empty?
|
13
13
|
def segment = @segment ||= parts&.[](0)
|
14
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 *
|
15
|
+
def identity = @identity ||= parts&.each_with_index&.reduce(0) { |acc, (p, i)| acc ^ (p.object_id) << (i * 8) }
|
16
16
|
def map(&block) = empty? ? self : Path.new(parts: parts.map(&block))
|
17
|
+
def shifted_id = @shifted_id ||= object_id << 32
|
17
18
|
|
18
19
|
def merge(other)
|
19
20
|
(@merged ||= {})[other] ||= \
|
@@ -5,6 +5,8 @@ module LazyGraph
|
|
5
5
|
POINTER_POOL = []
|
6
6
|
|
7
7
|
StackPointer = Struct.new(:parent, :frame, :depth, :key, :root) do
|
8
|
+
def shifted_id = @shifted_id ||= object_id << 32
|
9
|
+
|
8
10
|
def push(frame, key)
|
9
11
|
(POINTER_POOL.pop || StackPointer.new).tap do |pointer|
|
10
12
|
pointer.parent = self
|
data/lib/lazy_graph/version.rb
CHANGED