lazy_graph 0.1.6 → 0.2.0

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: 2cb285016525832f149dd41b91340c71f6f6c68d7aebcc0de6376dbf81c71cd8
4
- data.tar.gz: fc0bee81e2e2b45dfad51c822c421d0496bda877d8e77598d29875b66f261286
3
+ metadata.gz: ab53c56801cc3d5974f3ede700635376e2d306d7f1427f7001d30ba1a99907e3
4
+ data.tar.gz: 8ee2894b379254714c2a9bc69f37af7c30397df606ae7a737c17f926ff59d8da
5
5
  SHA512:
6
- metadata.gz: 0d156ef5344d260232771fa272d5a318342a46293a5991abb9c47948fd0b79c096f2f1f219cf9383a677435ec0e9e9f1bc95011b254d9828e2b19b86f991a5fe
7
- data.tar.gz: e9c44f0ff222e41ad1992703b3f8adf1a4424f073bc80b89bb1d0f85f515fbe8413d023f9072141fbeaff5b242bf9e91fd415c43b8445cc10d2754a17af73b0a
6
+ metadata.gz: '038a530b89df0972ec9d4bd366fe05094ae07916a2a42dcff6aff26480aa6d395ca2ca8caa9656d1733f8acb541205dd27eedc38a49af46204b897a894a0737f'
7
+ data.tar.gz: 79d7482d2d6f32aecfc4916a02e0357789675396c93c0245f0d6a79a052dbd1036797025ea8a70175aa5ae256500fe95191521531f96a91b4d27035073b956b1
data/README.md CHANGED
@@ -41,11 +41,13 @@
41
41
 
42
42
  ## Introduction
43
43
 
44
- **LazyGraph** is an ideal tool for building efficient, complex rules engines and optionally exposing these as stateless HTTP services.
44
+ **LazyGraph** is an ideal tool for building complex and efficient rules engines, and optionally comes with batteries included for
45
+ exposing these as stateless HTTP services using an opinionated set of defaults.
46
+
45
47
  Unlike traditional rules engines which utilize facts to manipulate stateful memory,
46
- LazyGraph encodes rules into a stateless and declarative knowledge graph.
48
+ LazyGraph encodes rules into a stateless, declarative knowledge graph.
47
49
 
48
- A LazyGraph is a similar to a limited [**JSON Schema**](https://json-schema.org/), which allows a single structure
50
+ A LazyGraph is similar to a limited [**JSON Schema**](https://json-schema.org/), which allows a single structure
49
51
  to define both the shape of your data domain, and all of the rules that should run within it.
50
52
 
51
53
  To use it, you can:
@@ -70,6 +72,147 @@ The `LazyGraph` library also includes:
70
72
  - An optional **HTTP server** to serve dynamic computations from a simple endpoint
71
73
  (serve any collection of LazyGraph builders as a stateless rules microservice, no additional code required)!.
72
74
 
75
+ ## Elevator Pitch
76
+
77
+ ```ruby
78
+ require 'lazy_graph'
79
+
80
+ module ShoppingCartTotals
81
+ module API
82
+ class V1 < LazyGraph::Builder
83
+ rules_module :cart do
84
+ array :items do
85
+ string :name
86
+ integer :quantity
87
+ number :price
88
+ number :unit_total, rule: '${quantity} * ${price}'
89
+ end
90
+
91
+ object :coupon_codes, invisible: true, rule: :valid_coupons do
92
+ object :".*", pattern_property: true do
93
+ number :discount_abs
94
+ number :discount_percent
95
+ number :min_total
96
+ one_of [
97
+ { required: [:discount_abs] },
98
+ { required: %i[discount_percent min_total] }
99
+ ]
100
+ end
101
+ end
102
+
103
+ string :applied_coupon, default: ''
104
+ number :gross, rule: '${items.unit_total}.sum'
105
+ number :discount, rule: 'apply_coupon_code(${coupon_codes[applied_coupon]}, ${gross})'
106
+ number :net, rule: '${gross} - ${discount}'
107
+ number :gst, rule: '(${net} * 0.1).round(2)'
108
+ number :total, rule: '${net} + ${gst}'
109
+ end
110
+ end
111
+ end
112
+
113
+ module CouponHelpers
114
+ module_function
115
+
116
+ def valid_coupons
117
+ {
118
+ '10OFF' => { discount_abs: 10 },
119
+ '15%OVER100' => { discount_percent: 15, min_total: 100 },
120
+ '20%OVER200' => { discount_percent: 20, min_total: 200 }
121
+ }
122
+ end
123
+
124
+ def apply_coupon_code(coupon_code, net)
125
+ return 0 unless coupon_code
126
+
127
+ coupon_code[:discount_abs] || net > coupon_code[:min_total] ? net * coupon_code[:discount_percent] / 100.0 : 0
128
+ end
129
+ end
130
+
131
+ API::V1.register_helper_modules(CouponHelpers)
132
+ include LazyGraph.bootstrap_app!(reload_paths: [])
133
+ end
134
+ ```
135
+
136
+ With just the above, we've defined our set of rules for computing GST and cart total, given a set of items.
137
+ We can now:
138
+ * Invoke this module directly from Ruby code, e.g.
139
+
140
+ ```ruby
141
+ ShoppingCartTotals::API::V1.cart.eval!({
142
+ "items": [
143
+ {"quantity": 2, "price": 200},
144
+ {"quantity": 2, "price": 5}
145
+ ],
146
+ "applied_coupon": "15%OVER100"
147
+ }).get('[total,net,discount]')
148
+
149
+ # => {total: 383.35, net: 348.5, discount: 61.5}
150
+ ```
151
+
152
+ * Expose this same service via an efficient, stateless HTTP API
153
+ e.g.
154
+
155
+ ```bash
156
+ $ bundle exec ruby shopping_cart_totals.rb
157
+ Starting single-process server on port 9292...
158
+ [PID 67702] Listening on port 9292...
159
+ ```
160
+
161
+ ```bash
162
+ $ RACK_ENV=production bundle exec ruby shopping_cart_totals.rb
163
+ Starting Raxx server with 8 processes on port 9292...
164
+ [PID 67791] Listening on port 9292...
165
+ [PID 67792] Listening on port 9292...
166
+ [PID 67793] Listening on port 9292...
167
+ [PID 67794] Listening on port 9292...
168
+ [PID 67795] Listening on port 9292...
169
+ [PID 67796] Listening on port 9292...
170
+ [PID 67797] Listening on port 9292...
171
+ [PID 67799] Listening on port 9292...
172
+ ```
173
+
174
+ ```bash
175
+ $ curl http://localhost:9292/api/v1 -XPOST -d '{
176
+ "modules": "cart",
177
+ "context": {
178
+ "items": [
179
+ {"quantity": 2, "price": 200},
180
+ {"quantity": 2, "price": 5}
181
+ ],
182
+ "applied_coupon": "15%OVER100"
183
+ }
184
+ }' | jq
185
+
186
+ {
187
+ "type": "success",
188
+ "result": {
189
+ "output": {
190
+ "items": [
191
+ {
192
+ "quantity": 2,
193
+ "price": 200,
194
+ "unit_total": 400
195
+ },
196
+ {
197
+ "quantity": 2,
198
+ "price": 5,
199
+ "unit_total": 10
200
+ }
201
+ ],
202
+ "applied_coupon": "15%OVER100",
203
+ "gross": 410,
204
+ "discount": 61.5,
205
+ "net": 348.5,
206
+ "gst": 34.85,
207
+ "total": 383.35
208
+ }
209
+ }
210
+ }
211
+
212
+ ```
213
+ The above showcases a selection of the most compelling features of LazyGraph in a simple single-file implementation, but there's much more to see.
214
+ Read on to learn more...
215
+
73
216
  ## Features
74
217
 
75
218
  - **Lazy Evaluation & Caching**
@@ -81,6 +224,7 @@ The `LazyGraph` library also includes:
81
224
  - **Debug Trace**
82
225
  The order in which recursive calculations are processed is not always obvious.
83
226
  LazyGraph is able to provide a detailed trace of exactly, when and how each value was computed.
227
+ Output from LazyGraph is transparent and traceable.
84
228
 
85
229
  - **Rich Querying Syntax**
86
230
  Extract exactly what you need from your model with an intuitive path-like syntax. Support for nested properties, arrays, indexing, ranges, wildcards, and more.
@@ -125,19 +269,23 @@ In this section, we’ll explore how to set up a minimal LazyGraph use case:
125
269
 
126
270
  ### Defining a Schema
127
271
 
128
- A LazyGraph schema looks like a standard JSON Schema, but with a few key differences:
272
+ A LazyGraph schema looks like a standard JSON Schema, you can build LazyGraph schemas
273
+ efficiently using the builder DSL, but can just as easily define one from a plain-old Ruby hash.
274
+
275
+ There are a few key differences between a JSON schema and a LazyGraph schema:
276
+
129
277
  * The `rule` property, which defines how to compute derived fields.
130
278
  - `rule:`
131
- Defines a that computes this property’s value, if not given, referencing other fields in the graph.
132
- * The schema also does not support computation across node types of `oneOf`, `allOf`, `anyOf`, `not`, or references
279
+ Defines a rule that computes this property’s value, if not given, referencing other fields in the graph.
280
+
281
+ * The schema also *does not* support computation across node types of `oneOf`, `allOf`, `anyOf`, `not`, or references
133
282
  (read examples for alternative mechanisms for achieving similar flexibility in your live schema)
134
283
 
135
284
  Any field (even object and array fields) can have a `rule` property.
136
285
 
137
- Values at this property are lazily computed by the rule, if not present in the input.
138
- However, if the value is present in the input, the rule is not triggered.
139
- This makes a LazyGraph highly flexible.
140
- (you can override *absolutely any* step in a complex computation graph, if you choose).
286
+ Values at this property are lazily computed, according the rule, if not present in the input.
287
+ However, if the value is present in the input, the rule is not triggered, which makes a LazyGraph highly flexible as
288
+ you can override *absolutely any* computed step or input in a complex computation graph, if you choose.
141
289
 
142
290
  Here’s a simple **shopping cart** example:
143
291
 
@@ -191,7 +339,7 @@ cart_graph = cart_schema.to_lazy_graph
191
339
 
192
340
  ### Providing Input
193
341
 
194
- Next, we create an **input document** that partially fills out the schema. For instance:
342
+ Once you've defined a `LazyGraph`, you should feed it an **input document** that partially fills out the schema. For instance:
195
343
 
196
344
  ```ruby
197
345
  input_data = {
@@ -209,7 +357,8 @@ input_data = {
209
357
 
210
358
  ### Querying the Graph
211
359
 
212
- To compute derived fields and extract results, we can do:
360
+ Then, to compute derived fields and extract results, we can query our lazy graph instance, to efficiently
361
+ resolve subsections of the graph (or just resolve the whole thing!).
213
362
 
214
363
  ```ruby
215
364
  # Create the graph and run the query:
@@ -287,7 +436,7 @@ end
287
436
 
288
437
  # Then we can build a cart schema, feed input, and query:
289
438
  cart_schema = ShoppingCart::CartBuilder.cart_base
290
- context = cart_schema.context({
439
+ context = cart_schema.feed({
291
440
  cart: {
292
441
  items: [
293
442
  { name: 'Widget', price: 10.0, quantity: 2 },
@@ -313,7 +462,9 @@ rules_module :stock do
313
462
  end
314
463
  ```
315
464
 
316
- You can then merge these, by chaining module builder calls
465
+ You can easily merge these, by chaining module builder calls.
466
+ This merging capability is particularly powerful if you have a very large business domain with many overlapping concerns.
467
+ You can allow the caller to dynamically compose and query any combination of these sub-domains, as required.
317
468
 
318
469
  ```ruby
319
470
  # Combine two modules
@@ -337,6 +488,8 @@ This graph-based approach means you can nest derived fields deeply without worry
337
488
  There are several different ways to define Rules in a LazyGraph.
338
489
  The most portable way (using plain old JSON) is to define a rule as a string that references other properties in the schema using
339
490
  ${} placeholder syntax.
491
+
492
+ ##### Simple inline rules
340
493
  E.g.
341
494
 
342
495
  ```ruby
@@ -361,12 +514,13 @@ rule: {
361
514
  }
362
515
  ```
363
516
 
517
+ ##### Block Rules
364
518
  The most expressive way to define rules is to use a Ruby block, proc or lambda.
365
- The arguments to the block are automatically resolved as inputs into the block.
366
- You can use keyword arguments to map paths to the input names.
519
+ This is also the recommended approach for any rules that are more than a simple expression.
520
+
521
+ The arguments to the block are *automatically* resolved and fed as inputs into the block.
522
+ You can use keyword arguments to map paths to the input names (only necessary if the input name differs from the resolved input path)
367
523
 
368
- *Note:* Block rules cannot be define from inside a REPL, as LazyGraph needs to read these from disk
369
- to be able to include the source code inside debug outputs.
370
524
  ```ruby
371
525
  # The input price is resolved to the value at path: 'price'
372
526
  # The input quantity is resolved to the value at path: 'quantity'
@@ -376,7 +530,11 @@ rule: ->(price, quantity, store_details: store.details) {
376
530
  }
377
531
  ```
378
532
 
379
- Resolutions are relative to the current node.
533
+ *Note:* Block rules *cannot* be defined from inside a REPL, as LazyGraph needs to read and interpret these blocks from the source code
534
+ on the filesystem to be able to perform resolution and to include the original source code inside debug outputs.
535
+
536
+ Resolution is performed relative to the current node.
537
+
380
538
  I.e. it will look for the path in the current node, and then in the parent node, and so on, until it finds a match.
381
539
  If you wish to make an absolute reference, you can prefix the path with a `$` to indicate that it should start at the root of the schema.
382
540
  Note, inside lambda rules, absolute references begin with a _ instead.
@@ -388,10 +546,16 @@ rule: ->(store_details: _.store.details) {
388
546
  }
389
547
  ```
390
548
 
391
- Inside rules, the scope is set such that you are able to freely access any other node in the computation graph.
549
+ Within the body of a rule, a full binding/context stack is populated, from the current node up to the root node
550
+ and used for resolution of any variables.
551
+ This means that within a rule you exist "within" the graph, and are able to freely access any other node in the computation graph.
552
+
392
553
  Just type a variable by name and it will automatically be recursively resolved to the correct value.
554
+
393
555
  *However* it is essential that you explicitly define all inputs to the rule to ensure resolution is correct,
394
- as LazyGraph will not automatically resolve any variables that are dynamically accessed.
556
+ as LazyGraph will not automatically resolve any variables that are dynamically accessed, meaning any variables that are
557
+ generated by rules may *not yet* have been populated when accessed from within a rule, unless it's been explicitly indicated as dependency.
558
+
395
559
  This is advanced functionality, and should be used with caution. In general, it is best to define all input dependencies explicitly.
396
560
  You can put a breakpoint inside a lambda rule to inspect the current scope and understand what is available to you.
397
561
  Check out:
@@ -408,7 +572,8 @@ If you pass `debug: true`, the output will contain an "output_trace" array,
408
572
  containing a detailed, ordered log of how each derived field was computed (inputs, calc and outputs).
409
573
 
410
574
  ```ruby
411
- context = cart_schema.to_graph_ctx({
575
+ cart_builder = ShoppingCart::CartBuilder.cart_base
576
+ context = cart_builder.feed({
412
577
  cart: {
413
578
  items: [
414
579
  { name: 'Widget', price: 10.0, quantity: 2 },
@@ -416,28 +581,106 @@ context = cart_schema.to_graph_ctx({
416
581
  ]
417
582
  }
418
583
  }, debug: true)
419
- result = context['cart.cart_total'] # triggers computations
420
584
 
421
- puts result[:debug_trace]
585
+ # Get debug output
586
+ context.debug("cart.cart_total")
587
+ # =>
588
+ # [{output: :"$.cart.items[0].total", result: 20.0, inputs: {price: 10.0, quantity: 2}, calc: "${price} * ${quantity}", location: "$.cart.items[0]"},
589
+ # {output: :"$.cart.items[1].total", result: 1.0, inputs: {price: 1.0, quantity: 1}, calc: "${price} * ${quantity}", location: "$.cart.items[1]"},
590
+ # {output: :"$.cart.cart_total", result: 21.0, inputs: {"items.total": [20.0, 1.0]}, calc: "${items.total}.sum", location: "$.cart"}]
591
+
592
+ # Alternatively, use #resolve to get a hash with both :output and :debug_trace keys populated
593
+ context.resolve("cart.cart_total")
594
+
595
+ # context.resolve("cart.cart_total")
422
596
  # =>
423
- # [{output: :"$.cart.items[0].total",
424
- # result: 20.0,
425
- # inputs: {aa22c03893cb0018e: 10.0, a2fb52b44379212e4: 2},
426
- # calc: ["aa22c03893cb0018e * a2fb52b44379212e4"],
427
- # location: "$.cart.items[0]"},
428
- # {output: :"$.cart.items[1].total",
429
- # result: 1.0,
430
- # inputs: {aa22c03893cb0018e: 1.0, a2fb52b44379212e4: 1},
431
- # calc: ["aa22c03893cb0018e * a2fb52b44379212e4"],
432
- # location: "$.cart.items[1]"},
433
- # {output: :"$.cart.cart_total", result: 21.0, inputs: {item_totals: [20.0, 1.0]}, calc: ["item_totals.sum"], location: "$.cart"}]
597
+ # {output: 21.0,
598
+ # debug_trace:
599
+ # [{output: :"$.cart.items[0].total", result: 20.0, inputs: {price: 10.0, quantity: 2}, calc: "${price} * ${quantity}", location: "$.cart.items[0]"},
600
+ # {output: :"$.cart.items[1].total", result: 1.0, inputs: {price: 1.0, quantity: 1}, calc: "${price} * ${quantity}", location: "$.cart.items[1]"},
601
+ # {output: :"$.cart.cart_total", result: 21.0, inputs: {"items.total": [20.0, 1.0]}, calc: "${items.total}.sum", location: "$.cart"}]}
434
602
  ```
435
603
 
436
604
  In cases where you accidentally create **circular dependencies**, LazyGraph will log warnings to the debug logs, and detect and break infinite loops
437
605
  in the dependency resolution, ensuring that the remainder of the graph is still computed correctly.
438
606
 
439
607
  ### Conditional Sub-Graphs
440
- [TODO]
608
+
609
+ Nodes within a graph, can be conditionally evaluated, based on properties elsewhere in the graph.
610
+ This can help you avoid excessive computation, and support polymorphism in outputs.
611
+ To use this functionality within the Builder dsl, use the #object_conditional helper.
612
+
613
+ E.g.
614
+
615
+ ```ruby
616
+ module ConversionAPI
617
+ class Rgb < LazyGraph::Builder
618
+ rules_module :rgb_converter do
619
+ %i[h s l g r b c m y k].each { number _1 }
620
+ string :mode, enum: %i[hsl cmyk rgb]
621
+ one_of [
622
+ { required: %i[r g b], properties: { mode: { const: 'rgb' } } },
623
+ { required: %i[h s l], properties: { mode: { const: 'hsl' } } },
624
+ { required: %i[c m y k], properties: { mode: { const: 'cmyk' } } }
625
+ ]
626
+
627
+ object_conditional :color do
628
+ matches :hsl, mode: 'hsl' do
629
+ array :rgb, type: :number, rule: lambda { |h, s, l|
630
+ a = s * [l, 1 - l].min
631
+ f = ->(n, k = (n + h / 30) % 12) { l - a * [[k - 3, 9 - k, 1].min, -1].max }
632
+ [255 * f.call(0), 255 * f.call(8), 255 * f.call(4)]
633
+ }
634
+ end
635
+
636
+ matches :cmyk, mode: 'cmyk' do
637
+ array :rgb, type: :number, rule: lambda { |c, m, y, k|
638
+ f = ->(x, k) { 255 * (1 - x) * (1 - k) }
639
+ [f.call(c, k), f.call(m, k), f.call(y, k)]
640
+ }
641
+ end
642
+
643
+ matches :rgb, mode: 'rgb' do
644
+ array :rgb, type: :number, rule: :"[${r},${g},${b}]"
645
+ end
646
+
647
+ array :rgb, type: :number
648
+ end
649
+ end
650
+ end
651
+
652
+ include LazyGraph.bootstrap_app!
653
+ end
654
+
655
+ ```
656
+
657
+ ```bash
658
+ $ bundle exec ruby converter.rb
659
+ # [PID 91560] Listening on port 9292...
660
+
661
+ $ curl -XPOST http://localhost:9292/rgb -d '{
662
+ "modules": "rgb_converter",
663
+ "query": "color.rgb",
664
+ "context": {
665
+ "mode": "hsl",
666
+ "h": 100,
667
+ "s": 0.2,
668
+ "l": 0.5
669
+ }
670
+ }' | jq
671
+
672
+ # {
673
+ # "type": "success",
674
+ # "result": {
675
+ # "output": [
676
+ # 127.5,
677
+ # 153.0,
678
+ # 102.0
679
+ # ]
680
+ # }
681
+ #}
682
+ ```
683
+
441
684
 
442
685
  ### Advanced Path Syntax
443
686
 
@@ -447,7 +690,7 @@ LazyGraph’s query engine supports a flexible path notation:
447
690
  - **Object bracket expansions**: If a property is enclosed in `[keyName]`, the key and value are kept together when extracting partial JSON. For example, `"cart[items]"` yields `{"cart"=>{"items"=>[...]}}`.
448
691
  - **Array indexing**: `"items[0]"`, `"items[0..2]"`, `"items[*]"` (all items), or `"items[0, 2, 4]"` for picking multiple indexes.
449
692
  - **Ranged queries**: `"items[1..3]"` returns a slice from index 1 to 3 inclusive.
450
- - **Root Queries**: 'Useful inside rules for depdencies that are not relative to the current node. E.g. `"$.cart.items[0].total"` (or `_.cart.items[0].total` inside proc rules).
693
+ - **Root Queries**: 'Useful inside rules for dependencies that are not relative to the current node. E.g. `"$.cart.items[0].total"` (or `_.cart.items[0].total` inside proc rules).
451
694
 
452
695
  ### LazyGraph Server
453
696
 
@@ -464,7 +707,6 @@ A minimal example might look like:
464
707
 
465
708
  ```ruby
466
709
  require 'lazy_graph'
467
- require 'lazy_graph/server'
468
710
 
469
711
  module CartAPI
470
712
  VERSION = '0.1.0'
@@ -512,24 +754,17 @@ module CartAPI
512
754
  end
513
755
 
514
756
  # Bootstrap our builder group.
515
- include LazyGraph::BuilderGroup.bootstrap!(reload_paths: File.join(__dir__, 'cart_api/**/*.rb'))
757
+ include LazyGraph.bootstrap_app!
516
758
  end
517
759
  ```
518
760
 
519
- `config.ru`
520
- ```ruby
521
- require_relative 'lib/paybun'
522
-
523
- run CartAPI.server
524
- ```
525
-
526
761
  Then send requests like:
527
762
 
528
763
  ```bash
529
764
  curl -X POST http://localhost:9292/cart/v1 \
530
765
  -H 'Content-Type: application/json' \
531
766
  -d '{
532
- "modules": { "cart" : {}, "stock": {} },
767
+ "modules": ["cart", "stock"],
533
768
  "query": "cart.cart_total",
534
769
  "context": {
535
770
  "cart": {
data/Rakefile CHANGED
@@ -1,4 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
- task default: %i[]
4
+ require 'minitest/test_task'
5
+ require 'debug'
6
+
7
+ Minitest::TestTask.create(:test) do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.warning = false
11
+ t.test_globs = ['test/**/*_test.rb']
12
+ t.test_prelude = 'require "helpers/test_helper.rb"'
13
+ end
14
+
15
+ task default: :test
@@ -0,0 +1,41 @@
1
+ require 'debug'
2
+ require 'lazy_graph'
3
+
4
+ module ConversionAPI
5
+ class Rgb < LazyGraph::Builder
6
+ rules_module :rgb_converter do
7
+ %i[h s l g r b c m y k].each { number _1 }
8
+ string :mode, enum: %i[hsl cmyk rgb]
9
+ one_of [
10
+ { required: %i[r g b], properties: { mode: { const: 'rgb' } } },
11
+ { required: %i[h s l], properties: { mode: { const: 'hsl' } } },
12
+ { required: %i[c m y k], properties: { mode: { const: 'cmyk' } } }
13
+ ]
14
+
15
+ object_conditional :color do
16
+ matches :hsl, mode: 'hsl' do
17
+ array :rgb, type: :number, rule: lambda { |h, s, l|
18
+ a = s * [l, 1 - l].min
19
+ f = ->(n, k = (n + h / 30) % 12) { l - a * [[k - 3, 9 - k, 1].min, -1].max }
20
+ [255 * f.call(0), 255 * f.call(8), 255 * f.call(4)]
21
+ }
22
+ end
23
+
24
+ matches :cmyk, mode: 'cmyk' do
25
+ array :rgb, type: :number, rule: lambda { |c, m, y, k|
26
+ f = ->(x, k) { 255 * (1 - x) * (1 - k) }
27
+ [f.call(c, k), f.call(m, k), f.call(y, k)]
28
+ }
29
+ end
30
+
31
+ matches :rgb, mode: 'rgb' do
32
+ array :rgb, type: :number, rule: :"[${r},${g},${b}]"
33
+ end
34
+
35
+ array :rgb, type: :number
36
+ end
37
+ end
38
+ end
39
+
40
+ include LazyGraph.bootstrap_app!
41
+ end
@@ -15,8 +15,8 @@ class PerformanceBuilder < LazyGraph::Builder
15
15
 
16
16
  object :position, rule: :"$.positions[position_id]"
17
17
  object :pay_schedule, rule: :'pay_schedules[pay_schedule_id]'
18
- number :pay_rate, rule: :"position.pay_rate"
19
- string :employee_id, rule: :id
18
+ number :pay_rate, rule: :"${position.pay_rate}"
19
+ string :employee_id, rule: :"${id}"
20
20
  end
21
21
  end
22
22
 
@@ -94,7 +94,6 @@ def benchmark_ips_n(n, debug: false, validate: false)
94
94
  end
95
95
 
96
96
  def console_n(n, debug: false, validate: false)
97
- require 'debug'
98
97
  graph = PerformanceBuilder.performance.build!(debug: debug, validate: validate)
99
98
  employees = gen_employees(n)
100
99
  result = graph.context(employees).get('')
@@ -105,5 +104,6 @@ case ARGV[0]
105
104
  when 'ips' then benchmark_ips_n(ARGV.fetch(1, 1000).to_i)
106
105
  when 'memory' then memory_profile_n(ARGV.fetch(1, 1000).to_i)
107
106
  when 'console' then console_n(ARGV.fetch(1, 1000).to_i, debug: true)
108
- else profile_n(ARGV.fetch(1, 100_000).to_i)
107
+ when 'profile' then profile_n(ARGV.fetch(1, 100_000).to_i)
108
+ else nil
109
109
  end
@@ -0,0 +1,56 @@
1
+ require 'lazy_graph'
2
+
3
+ module ShoppingCartTotals
4
+ module API
5
+ class V1 < LazyGraph::Builder
6
+ rules_module :cart do
7
+ array :items do
8
+ string :name
9
+ integer :quantity
10
+ number :price
11
+ number :unit_total, rule: '${quantity} * ${price}'
12
+ end
13
+
14
+ object :coupon_codes, invisible: true, rule: :valid_coupons do
15
+ object :".*", pattern_property: true do
16
+ number :discount_abs
17
+ number :discount_percent
18
+ number :min_total
19
+ one_of [
20
+ { required: [:discount_abs] },
21
+ { required: %i[discount_percent min_total] }
22
+ ]
23
+ end
24
+ end
25
+
26
+ string :applied_coupon, default: ''
27
+ number :gross, rule: '${items.unit_total}.sum'
28
+ number :discount, rule: 'apply_coupon_code(${coupon_codes[applied_coupon]}, ${gross})'
29
+ number :net, rule: '${gross} - ${discount}'
30
+ number :gst, rule: '(${net} * 0.1).round(2)'
31
+ number :total, rule: '${net} + ${gst}'
32
+ end
33
+ end
34
+ end
35
+
36
+ module CouponHelpers
37
+ module_function
38
+
39
+ def valid_coupons
40
+ {
41
+ '10OFF' => { discount_abs: 10 },
42
+ '15%OVER100' => { discount_percent: 15, min_total: 100 },
43
+ '20%OVER200' => { discount_percent: 20, min_total: 200 }
44
+ }
45
+ end
46
+
47
+ def apply_coupon_code(coupon_code, net)
48
+ return 0 unless coupon_code
49
+
50
+ coupon_code[:discount_abs] || net > coupon_code[:min_total] ? net * coupon_code[:discount_percent] / 100.0 : 0
51
+ end
52
+ end
53
+
54
+ API::V1.register_helper_modules(CouponHelpers)
55
+ include LazyGraph.bootstrap_app!(reload_paths: [])
56
+ end