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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df09bd7d0c4f8a82c1cdbaf387dfc6e18ec9f9b6f492a1aa17b2942f1e14a947
4
- data.tar.gz: c9887fff1a0eacb38b5ec426f13ce5a8fc23e7d6602a1876a398773628a3461e
3
+ metadata.gz: e561f823062b3d3aedb3274a14d80e6bf1a17547bc3fbcb691cab37fc22ee014
4
+ data.tar.gz: c353086cec79a6ceaf1c38db742f222474172be184307dab69f00c5f054eb081
5
5
  SHA512:
6
- metadata.gz: a90efb82622865574cffd51c996ceeaec5f8f29534da07869ef3c0bb2de3056031ec4c0357b41a3e4fe0390e4e8e364bd07b76f1073b08a8beb95124f7eda301
7
- data.tar.gz: 508a463c441ebd661871d8655e52dd230bbdc2e4458898a6399099dcc6ef70d90455042f5e960c471f64c77fb56b1456a2c8919b6f0ac28ba769b0a2374fb8a0
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
- items do
277
- string :name, required: true
278
- number :price, default: 1.0
279
- number :quantity, default: 1
280
- number :total, rule: '${price} * ${quantity}'
281
- end
276
+ string :name, required: true
277
+ number :price, default: 1.0
278
+ number :quantity, default: 1
279
+ number :total, rule: '${price} * ${quantity}'
282
280
  end
283
281
 
284
282
  number :cart_total, rule: '${items.total}.sum'
@@ -301,6 +299,30 @@ context = cart_schema.context({
301
299
  puts context['cart.cart_total'] # => 21.0
302
300
  ```
303
301
 
302
+ We can create any number of modules within a builder, and then merge them together in any combination to create a final schema.
303
+ E.g.
304
+
305
+ ```ruby
306
+ # A second module that can be merged into the schema
307
+ rules_module :stock do
308
+ object :stock do
309
+ number :available, default: 100
310
+ number :reserved, default: 0
311
+ number :total, rule: '${available} - ${reserved}'
312
+ end
313
+ end
314
+ ```
315
+
316
+ You can then merge these, by chaining module builder calls
317
+
318
+ ```ruby
319
+ # Combine two modules
320
+ cart_schema = ShoppingCart::CartBuilder.cart_base.stock.build! # (also accepts optional args like :debug and :validate)
321
+
322
+ # Use just a single module
323
+ cart_schema = ShoppingCart::CartBuilder.cart_base.build!
324
+ ```
325
+
304
326
  ### Rules and Dependency Resolution
305
327
 
306
328
  Rules let you define logic for computing new values from existing ones. LazyGraph:
@@ -371,6 +393,14 @@ Just type a variable by name and it will automatically be recursively resolved t
371
393
  *However* it is essential that you explicitly define all inputs to the rule to ensure resolution is correct,
372
394
  as LazyGraph will not automatically resolve any variables that are dynamically accessed.
373
395
  This is advanced functionality, and should be used with caution. In general, it is best to define all input dependencies explicitly.
396
+ You can put a breakpoint inside a lambda rule to inspect the current scope and understand what is available to you.
397
+ Check out:
398
+
399
+ `stack_ptr` - The current stack pointer, which is the current node in the graph.
400
+ `stack_ptr.parent` - The parent node in the graph (you can traverse up the graph by following the parent pointers).
401
+ `stack_ptr.key` - The key in the parent node where the current node is stored (e.g. an index in an array or property name)
402
+ `stack_ptr.frame` - The current frame in the graph, which contains actual graph data.
403
+ `itself` - The current node in the graph (same as stack_ptr.frame)
374
404
 
375
405
  ### Debug Mode & Recursive Dependency Detection
376
406
 
@@ -406,6 +436,9 @@ puts result[:debug_trace]
406
436
  In cases where you accidentally create **circular dependencies**, LazyGraph will log warnings to the debug logs, and detect and break infinite loops
407
437
  in the dependency resolution, ensuring that the remainder of the graph is still computed correctly.
408
438
 
439
+ ### Conditional Sub-Graphs
440
+ [TODO]
441
+
409
442
  ### Advanced Path Syntax
410
443
 
411
444
  LazyGraph’s query engine supports a flexible path notation:
@@ -429,7 +462,6 @@ For situations where you want to serve rules over HTTP:
429
462
 
430
463
  A minimal example might look like:
431
464
 
432
- [TODO] Verify example.
433
465
  ```ruby
434
466
  require 'lazy_graph'
435
467
  require 'lazy_graph/server'
@@ -453,20 +485,29 @@ module CartAPI
453
485
  # - context: { cart: { items: [ { name: "Widget", price: 2.5, quantity: 4 } ] } } # The input data to the schema
454
486
  module Cart
455
487
  class V1 < LazyGraph::Builder
488
+
489
+ # A module that can be merged into the schema
456
490
  rules_module :cart_base do |foo:, bar:|
457
491
  object :cart do
458
492
  array :items, required: true do
459
- items do
460
- string :name, required: true
461
- number :price, default: 1.0
462
- number :quantity, default: 1
463
- number :total, rule: '${price} * ${quantity}'
464
- end
493
+ string :name, required: true
494
+ number :price, default: 1.0
495
+ number :quantity, default: 1
496
+ number :total, rule: '${price} * ${quantity}'
465
497
  end
466
498
 
467
499
  number :cart_total, rule: '${items.total}.sum'
468
500
  end
469
501
  end
502
+
503
+ # A second module that can be merged into the schema
504
+ rules_module :stock do
505
+ object :stock do
506
+ number :available, default: 100
507
+ number :reserved, default: 0
508
+ number :total, rule: '${available} - ${reserved}'
509
+ end
510
+ end
470
511
  end
471
512
  end
472
513
 
@@ -488,7 +529,7 @@ Then send requests like:
488
529
  curl -X POST http://localhost:9292/cart/v1 \
489
530
  -H 'Content-Type: application/json' \
490
531
  -d '{
491
- "modules": { "cart" : {} },
532
+ "modules": { "cart" : {}, "stock": {} },
492
533
  "query": "cart.cart_total",
493
534
  "context": {
494
535
  "cart": {
@@ -8,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
- items do
12
- string :id
13
- array :positions, required: true, default: [] do
14
- items do
15
- string :pay_schedule_id, invisible: true
16
- string :position_id, invisible: true
11
+ string :id
12
+ array :positions, required: true, default: [] do
13
+ string :pay_schedule_id, invisible: true
14
+ string :position_id, invisible: true
17
15
 
18
- object :position, rule: :"${$.positions[position_id]}" do
19
- number :base_rate
20
- number :salary
21
- number :bonus, rule: :'${base_rate} * 0.1'
22
- end
23
- object :pay_schedule, rule: :'${pay_schedules[pay_schedule_id]}'
24
- number :base_rate, rule: :"${position.base_rate}"
25
- string :employee_id, rule: :id
26
- end
27
- end
16
+ object :position, rule: :"${$.positions[position_id]}"
17
+ object :pay_schedule, rule: :'${pay_schedules[pay_schedule_id]}'
18
+ number :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
- properties: {},
267
- additionalProperties: false
266
+ **(
267
+ type == :object ? { properties: {}, additionalProperties: false } : {}
268
+ )
268
269
  }
269
270
  }
270
- yields(new_array, &blk)
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
@@ -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
@@ -7,6 +7,7 @@ module LazyGraph
7
7
  attr_accessor :ruleset, :input
8
8
 
9
9
  def initialize(graph, input)
10
+ input = HashUtils.deep_dup(input)
10
11
  HashUtils.deep_symbolize!(input)
11
12
  graph.validate!(input) if [true, 'input'].include?(graph.validate)
12
13
  @graph = graph
@@ -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!(json_schema).merge(type: :object)
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!(hash)
7
+ def deep_dup(hash)
8
8
  case hash
9
9
  when Hash
10
- hash = hash.dup
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!(value) }
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 ^ (path.object_id << 8)] ||= begin
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 = derived[:calc].to_s.lines unless derived[:calc].is_a?(Proc)
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 ^ (path.object_id << 8)] ||= begin
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.default_proc = ->(h, k) { k.is_a?(Symbol) ? nil : h[k.to_s.to_sym] }
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)
@@ -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 ^ (stack_memory.object_id << 8)
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 * 4) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LazyGraph
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lazy_graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters