lazy_graph 0.1.0 → 0.1.1
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 +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