lazy_graph 0.1.6 → 0.2.0
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 +282 -47
- data/Rakefile +12 -1
- data/examples/converter.rb +41 -0
- data/examples/performance_tests.rb +4 -4
- data/examples/shopping_cart_totals.rb +56 -0
- data/lib/lazy_graph/builder/dsl.rb +67 -29
- data/lib/lazy_graph/builder.rb +43 -23
- data/lib/lazy_graph/builder_group.rb +29 -18
- data/lib/lazy_graph/cli.rb +47 -0
- data/lib/lazy_graph/context.rb +10 -6
- data/lib/lazy_graph/environment.rb +6 -0
- data/lib/lazy_graph/graph.rb +5 -3
- data/lib/lazy_graph/hash_utils.rb +4 -1
- data/lib/lazy_graph/logger.rb +87 -0
- data/lib/lazy_graph/missing_value.rb +8 -0
- data/lib/lazy_graph/node/array_node.rb +3 -2
- data/lib/lazy_graph/node/derived_rules.rb +60 -18
- data/lib/lazy_graph/node/node_properties.rb +16 -3
- data/lib/lazy_graph/node/object_node.rb +9 -5
- data/lib/lazy_graph/node/symbol_hash.rb +15 -2
- data/lib/lazy_graph/node.rb +97 -39
- data/lib/lazy_graph/rack_app.rb +138 -0
- data/lib/lazy_graph/rack_server.rb +199 -0
- data/lib/lazy_graph/version.rb +1 -1
- data/lib/lazy_graph.rb +6 -8
- metadata +115 -11
- data/lib/lazy_graph/server.rb +0 -96
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab53c56801cc3d5974f3ede700635376e2d306d7f1427f7001d30ba1a99907e3
|
4
|
+
data.tar.gz: 8ee2894b379254714c2a9bc69f37af7c30397df606ae7a737c17f926ff59d8da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
48
|
+
LazyGraph encodes rules into a stateless, declarative knowledge graph.
|
47
49
|
|
48
|
-
A LazyGraph is
|
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,
|
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
|
-
|
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
|
138
|
-
However, if the value is present in the input, the rule is not triggered
|
139
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
366
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
424
|
-
#
|
425
|
-
#
|
426
|
-
#
|
427
|
-
#
|
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
|
-
|
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
|
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
|
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":
|
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
|
-
|
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
|
-
|
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
|