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 +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
|