lazy_graph 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,562 @@
1
+ <table>
2
+ <tr>
3
+ <td><img src="./logo.png" alt="logo" width="150"></td>
4
+ <td>
5
+ <h1 align="center">LazyGraph</h1>
6
+ <p align="center">
7
+ <a href="https://rubygems.org/gems/lazy_graph">
8
+ <img alt="GEM Version" src="https://img.shields.io/gem/v/lazy_graph?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
9
+ </a><br>
10
+ <a href="https://rubygems.org/gems/lazy_graph">
11
+ <img alt="GEM Downloads" src="https://img.shields.io/gem/dt/lazy_graph?color=168AFE&logo=ruby&logoColor=FE1616">
12
+ </a>
13
+ </p>
14
+ </td>
15
+ </tr>
16
+ </table>
17
+
18
+ # LazyGraph
19
+
20
+ <details>
21
+ <summary><strong>Table of Contents</strong></summary>
22
+
23
+ 1. [Introduction](#introduction)
24
+ 2. [Features](#features)
25
+ 3. [Installation](#installation)
26
+ 4. [Getting Started](#getting-started)
27
+ - [Defining a Schema](#defining-a-schema)
28
+ - [Providing Input](#providing-input)
29
+ - [Querying the Graph](#querying-the-graph)
30
+ 5. [Advanced Usage](#advanced-usage)
31
+ - [Builder DSL](#builder-dsl)
32
+ - [Derived Properties and Dependency Resolution](#derived-properties-and-dependency-resolution)
33
+ - [Debug Mode & Recursive Dependency Detection](#debug-mode--recursive-dependency-detection)
34
+ - [Advanced Path Syntax](#advanced-path-syntax)
35
+ - [LazyGraph Server](#lazygraph-server)
36
+ 6. [API Reference and Helpers](#api-reference-and-helpers)
37
+ 7. [Contributing](#contributing)
38
+ 8. [License](#license)
39
+
40
+ </details>
41
+
42
+ ## Introduction
43
+
44
+ **LazyGraph** is an ideal tool for building efficient, complex rules engines and optionally exposing these as stateless HTTP services.
45
+ Unlike traditional rules engines which utilize facts to manipulate stateful memory,
46
+ LazyGraph encodes rules into a stateless and declarative knowledge graph.
47
+
48
+ A LazyGraph is a similar to a limited [**JSON Schema**](https://json-schema.org/), which allows a single structure
49
+ to define both the shape of your data domain, and all of the rules that should run within it.
50
+
51
+ To use it, you can:
52
+
53
+ 1. Define a **JSON Schema-like** structure describing how data in your domain is structured, containing required, optional and derived properties.
54
+ - **Rules**: All properties that can be computed are marked with a `rule` property, which defines how to compute the value in plain old Ruby code.
55
+ The rule can reference other properties in the schema, allowing for complex, nested, and recursive computations.
56
+
57
+ 2. Feed an **input document** (JSON) that partially fills out the schema with actual data.
58
+
59
+ 3. Query the `LazyGraph` for computed outputs.
60
+
61
+ `LazyGraph` will:
62
+ * Validate that the input conforms to the schema’s structure and then
63
+ * Allow you to intelligently query the graph for information, lazily triggering the evaluation of rules (resolving dependencies in a single pass and caching results).
64
+
65
+ The final output is the queried slice of your JSON schema, filled with computed outputs.
66
+
67
+ The `LazyGraph` library also includes:
68
+
69
+ - A **Builder DSL** to dynamically compose targeted JSON Schemas in Ruby.
70
+ - An optional **HTTP server** to serve dynamic computations from a simple endpoint
71
+ (serve any collection of LazyGraph builders as a stateless rules microservice, no additional code required)!.
72
+
73
+ ## Features
74
+
75
+ - **Lazy Evaluation & Caching**
76
+ Derived fields are efficiently calculated, at-most-once, on-demand.
77
+
78
+ - **Recursive Dependency Check**
79
+ Automatically detects cycles in your derived fields and logs warnings/debug info if recursion is found.
80
+
81
+ - **Debug Trace**
82
+ The order in which recursive calculations are processed is not always obvious.
83
+ LazyGraph is able to provide a detailed trace of exactly, when and how each value was computed.
84
+
85
+ - **Rich Querying Syntax**
86
+ Extract exactly what you need from your model with an intuitive path-like syntax. Support for nested properties, arrays, indexing, ranges, wildcards, and more.
87
+
88
+ - **Composable Builder DSL**
89
+ Support dynamic creation of large, composable schemas in Ruby with a simplified syntax.
90
+
91
+ - **Optional HTTP Server**
92
+ Spin up an efficient server that exposes several dynamic lazy-graphs as endpoints, allowing you to:
93
+ * Select a dynamic schema
94
+ * Feed in inputs and a query
95
+ * Receive the computed JSON output
96
+ all at lightning speed.
97
+
98
+ ## Installation
99
+
100
+ Add this line to your application’s Gemfile:
101
+
102
+ ```ruby
103
+ gem 'lazy_graph'
104
+ ```
105
+
106
+ And then execute:
107
+
108
+ ```bash
109
+ bundle
110
+ ```
111
+
112
+ Or install it yourself:
113
+
114
+ ```bash
115
+ gem install lazy_graph
116
+ ```
117
+
118
+ ## Getting Started
119
+
120
+ In this section, we’ll explore how to set up a minimal LazyGraph use case:
121
+
122
+ 1. **Define** a JSON Schema (with LazyGraph’s extended properties and types).
123
+ 2. **Provide** an input document.
124
+ 3. **Query** the graph to retrieve computed data.
125
+
126
+ ### Defining a Schema
127
+
128
+ A LazyGraph schema looks like a standard JSON Schema, but with a few key differences:
129
+ * The `rule` property, which defines how to compute derived fields.
130
+ - `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
133
+ (read examples for alternative mechanisms for achieving similar flexibility in your live schema)
134
+
135
+ Any field (even object and array fields) can have a `rule` property.
136
+
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).
141
+
142
+ Here’s a simple **shopping cart** example:
143
+
144
+ ```ruby
145
+ require 'lazy_graph'
146
+
147
+ cart_schema = {
148
+ type: 'object',
149
+ properties: {
150
+ cart: {
151
+ type: 'object',
152
+ properties: {
153
+ items: {
154
+ type: 'array',
155
+ items: {
156
+ type: 'object',
157
+ properties: {
158
+ name: {
159
+ type: 'string'
160
+ },
161
+ price: {
162
+ type: 'number',
163
+ default: 1.0
164
+ },
165
+ quantity: {
166
+ type: 'number',
167
+ default: 1
168
+ },
169
+ total: {
170
+ type: 'number',
171
+ rule: '${price} * ${quantity}'
172
+ }
173
+ }
174
+ },
175
+ required: ['name']
176
+ },
177
+ cart_total: {
178
+ type: 'number',
179
+ rule: {
180
+ inputs: {item_totals: 'items.total'},
181
+ calc: 'item_totals.sum'
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ cart_graph = cart_schema.to_lazy_graph
190
+ ```
191
+
192
+ ### Providing Input
193
+
194
+ Next, we create an **input document** that partially fills out the schema. For instance:
195
+
196
+ ```ruby
197
+ input_data = {
198
+ cart: {
199
+ items: [
200
+ { name: 'Widget', price: 5.0, quantity: 2 },
201
+ { name: 'Gadget' }
202
+ ]
203
+ }
204
+ }
205
+ ```
206
+
207
+ - `Widget` is fully specified with `price=5.0` and `quantity=2`.
208
+ - `Gadget` is missing `price` and `quantity`, so it will use defaults (`1.0` for price and `1` for quantity).
209
+
210
+ ### Querying the Graph
211
+
212
+ To compute derived fields and extract results, we can do:
213
+
214
+ ```ruby
215
+ # Create the graph and run the query:
216
+ graph_context = cart_graph.context(input_data)
217
+
218
+ # If we query '' (empty string), we get the entire graph with computed values:
219
+ whole_output = graph_context['']
220
+ puts JSON.pretty_generate whole_output
221
+ # {
222
+ # "output": {
223
+ # "cart": {
224
+ # "items": [
225
+ # {
226
+ # "name": "Widget",
227
+ # "price": 5.0,
228
+ # "quantity": 2,
229
+ # "total": 10.0
230
+ # },
231
+ # {
232
+ # "name": "Gadget",
233
+ # "price": 1.0,
234
+ # "quantity": 1,
235
+ # "total": 1.0
236
+ # }
237
+ # ],
238
+ # "cart_total": 11.0
239
+ # }
240
+ # },
241
+ # "debug_trace": null
242
+ # }
243
+
244
+ # Query a specific path, e.g. "cart.items[0].total"
245
+ graph_context["cart.items[0].total"]
246
+ # => {output: 10.0, debug_trace: nil}
247
+
248
+ # e.g. "cart.items.total"
249
+ graph_context["cart.items.total"]
250
+ # => {output: [10.0, 1.0], debug_trace: nil}
251
+
252
+
253
+ # e.g. "cart.items[name, total]"
254
+ all_item_name_and_totals = graph_context["cart.items[name, total]"]
255
+ # => {output: [{name: "Widget", total: 10.0}, {name: "Gadget", total: 1.0}], debug_trace: nil}
256
+ ```
257
+
258
+ LazyGraph **recursively computes** any derived fields, referenced in the query.
259
+ If you query only a subset of the graph, only the necessary computations are triggered, allowing for very fast responses,
260
+ even on graphs with millions of interdependent nodes.
261
+
262
+ ## Advanced Usage
263
+
264
+ ### Builder DSL
265
+
266
+ Rather than manually writing JSON schemas, LazyGraph also offers a **Ruby DSL** for building them.
267
+ This is useful if your schema needs to be dynamic (variable based on inputs), has repeated patterns, or is built across multiple modules,
268
+ which you wish to allow a user to combine in different ways.
269
+
270
+ ```ruby
271
+ module ShoppingCart
272
+ class CartBuilder < LazyGraph::Builder
273
+ rules_module :cart_base do
274
+ object :cart do
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
282
+ end
283
+
284
+ number :cart_total, rule: '${items.total}.sum'
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ # Then we can build a cart schema, feed input, and query:
291
+ cart_schema = ShoppingCart::CartBuilder.cart_base
292
+ context = cart_schema.context({
293
+ cart: {
294
+ items: [
295
+ { name: 'Widget', price: 10.0, quantity: 2 },
296
+ { name: 'Thingamajig' }
297
+ ]
298
+ }
299
+ })
300
+
301
+ puts context['cart.cart_total'] # => 21.0
302
+ ```
303
+
304
+ ### Rules and Dependency Resolution
305
+
306
+ Rules let you define logic for computing new values from existing ones. LazyGraph:
307
+
308
+ 1. Gathers each property’s **dependencies** (in the example, `total` depends on `price` and `quantity`).
309
+ 2. Computes them **on-demand**, in a topological order, ensuring that any fields they rely on are already resolved.
310
+ 3. Caches the results so subsequent references don’t trigger re-computation.
311
+
312
+ This graph-based approach means you can nest derived fields deeply without worrying about explicit ordering. If a derived field depends on another derived field, LazyGraph naturally handles the chain.
313
+
314
+ #### Derived Rules DSL
315
+ There are several different ways to define Rules in a LazyGraph.
316
+ The most portable way (using plain old JSON) is to define a rule as a string that references other properties in the schema using
317
+ ${} placeholder syntax.
318
+ E.g.
319
+
320
+ ```ruby
321
+ rule: '${price} * ${quantity}'
322
+ ```
323
+
324
+ You can define the inputs separately from the calculation, which can be useful for more complex rules:
325
+
326
+ As an array if you do not need to map paths
327
+ ```ruby
328
+ rule: {
329
+ inputs: %[quantity],
330
+ calc: 'quantity.sum'
331
+ }
332
+ ```
333
+
334
+ As a hash of input names to resolution paths
335
+ ```ruby
336
+ rule: {
337
+ inputs: {item_totals: 'items.total'},
338
+ calc: 'item_totals.sum'
339
+ }
340
+ ```
341
+
342
+ The most expressive way to define rules is to use a Ruby block, proc or lambda.
343
+ The arguments to the block are automatically resolved as inputs into the block.
344
+ You can use keyword arguments to map paths to the input names.
345
+
346
+ *Note:* Block rules cannot be define from inside a REPL, as LazyGraph needs to read these from disk
347
+ to be able to include the source code inside debug outputs.
348
+ ```ruby
349
+ # The input price is resolved to the value at path: 'price'
350
+ # The input quantity is resolved to the value at path: 'quantity'
351
+ # The input store_details is resolved to the value at path: 'store.details'
352
+ rule: ->(price, quantity, store_details: store.details) {
353
+ price * quantity * store_details.discount
354
+ }
355
+ ```
356
+
357
+ Resolutions are relative to the current node.
358
+ 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.
359
+ 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.
360
+ Note, inside lambda rules, absolute references begin with a _ instead.
361
+
362
+ E.g.
363
+ ```ruby
364
+ rule: ->(store_details: _.store.details) {
365
+ store_details.discount * price * quantity
366
+ }
367
+ ```
368
+
369
+ Inside rules, the scope is set such that you are able to freely access any other node in the computation graph.
370
+ Just type a variable by name and it will automatically be recursively resolved to the correct value.
371
+ *However* it is essential that you explicitly define all inputs to the rule to ensure resolution is correct,
372
+ as LazyGraph will not automatically resolve any variables that are dynamically accessed.
373
+ This is advanced functionality, and should be used with caution. In general, it is best to define all input dependencies explicitly.
374
+
375
+ ### Debug Mode & Recursive Dependency Detection
376
+
377
+ If you pass `debug: true`, the output will contain an "output_trace" array,
378
+ containing a detailed, ordered log of how each derived field was computed (inputs, calc and outputs).
379
+
380
+ ```ruby
381
+ context = cart_schema.to_graph_ctx({
382
+ cart: {
383
+ items: [
384
+ { name: 'Widget', price: 10.0, quantity: 2 },
385
+ { name: 'Thingamajig' }
386
+ ]
387
+ }
388
+ }, debug: true)
389
+ result = context['cart.cart_total'] # triggers computations
390
+
391
+ puts result[:debug_trace]
392
+ # =>
393
+ # [{output: :"$.cart.items[0].total",
394
+ # result: 20.0,
395
+ # inputs: {aa22c03893cb0018e: 10.0, a2fb52b44379212e4: 2},
396
+ # calc: ["aa22c03893cb0018e * a2fb52b44379212e4"],
397
+ # location: "$.cart.items[0]"},
398
+ # {output: :"$.cart.items[1].total",
399
+ # result: 1.0,
400
+ # inputs: {aa22c03893cb0018e: 1.0, a2fb52b44379212e4: 1},
401
+ # calc: ["aa22c03893cb0018e * a2fb52b44379212e4"],
402
+ # location: "$.cart.items[1]"},
403
+ # {output: :"$.cart.cart_total", result: 21.0, inputs: {item_totals: [20.0, 1.0]}, calc: ["item_totals.sum"], location: "$.cart"}]
404
+ ```
405
+
406
+ In cases where you accidentally create **circular dependencies**, LazyGraph will log warnings to the debug logs, and detect and break infinite loops
407
+ in the dependency resolution, ensuring that the remainder of the graph is still computed correctly.
408
+
409
+ ### Advanced Path Syntax
410
+
411
+ LazyGraph’s query engine supports a flexible path notation:
412
+
413
+ - **Nested properties**: `"cart.items[0].total"`, `"cart.items[*].name"`, etc.
414
+ - **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"=>[...]}}`.
415
+ - **Array indexing**: `"items[0]"`, `"items[0..2]"`, `"items[*]"` (all items), or `"items[0, 2, 4]"` for picking multiple indexes.
416
+ - **Ranged queries**: `"items[1..3]"` returns a slice from index 1 to 3 inclusive.
417
+ - **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).
418
+
419
+ ### LazyGraph Server
420
+
421
+ For situations where you want to serve rules over HTTP:
422
+
423
+ 1. **Define** your schema(s) with the DSL or standard JSON approach.
424
+ 2. **Implement** a small server that:
425
+ - Instantiates the schema.
426
+ - Takes JSON input from a request.
427
+ - Runs a query (passed via a query parameter or request body).
428
+ - Returns the computed JSON object.
429
+
430
+ A minimal example might look like:
431
+
432
+ [TODO] Verify example.
433
+ ```ruby
434
+ require 'lazy_graph'
435
+ require 'lazy_graph/server'
436
+
437
+ module CartAPI
438
+ VERSION = '0.1.0'
439
+
440
+ # Add all classes that you want to expose in the API, as constants to the builder group module.
441
+ # These will turn into downcased, nested endpoints.
442
+ # E.g.
443
+ # /cart/v1
444
+ # - GET: Get module info
445
+ # - POST: Run a query
446
+ # Inputs: A JSON object with the following keys:
447
+ #
448
+ # - modules: { cart: {} } # The modules to merged into the combined schema,
449
+ # # you can pass multiple modules here to merge them into a single schema.
450
+ # The keys inside each module object, are passed as arguments to the rules_module dsl method
451
+ # to allow you to dynamically adjust your output schema based on API inputs.
452
+ # - query: "cart.cart_total" # The query to run. Can be a string, an array of strings or empty (in which case entire graph is returned)
453
+ # - context: { cart: { items: [ { name: "Widget", price: 2.5, quantity: 4 } ] } } # The input data to the schema
454
+ module Cart
455
+ class V1 < LazyGraph::Builder
456
+ rules_module :cart_base do |foo:, bar:|
457
+ object :cart do
458
+ 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
465
+ end
466
+
467
+ number :cart_total, rule: '${items.total}.sum'
468
+ end
469
+ end
470
+ end
471
+ end
472
+
473
+ # Bootstrap our builder group.
474
+ include LazyGraph::BuilderGroup.bootstrap!(reload_paths: File.join(__dir__, 'cart_api/**/*.rb'))
475
+ end
476
+ ```
477
+
478
+ `config.ru`
479
+ ```ruby
480
+ require_relative 'lib/paybun'
481
+
482
+ run CartAPI.server
483
+ ```
484
+
485
+ Then send requests like:
486
+
487
+ ```bash
488
+ curl -X POST http://localhost:9292/cart/v1 \
489
+ -H 'Content-Type: application/json' \
490
+ -d '{
491
+ "modules": { "cart" : {} },
492
+ "query": "cart.cart_total",
493
+ "context": {
494
+ "cart": {
495
+ "items": [
496
+ { "name": "Widget", "price": 2.5, "quantity": 4 }
497
+ ]
498
+ }
499
+ }
500
+ }'
501
+ ```
502
+
503
+ Response:
504
+
505
+ ```json
506
+ {
507
+ "type": "success",
508
+ "result": {
509
+ "output": 10.0
510
+ }
511
+ }
512
+ ```
513
+
514
+ ## Where LazyGraph Fits
515
+
516
+ If you’re coming from a background in any of the following technologies, you might find several familiar ideas reflected in LazyGraph:
517
+
518
+ * *Excel*: Formulas in spreadsheets operate similarly to derived fields—they reference other cells (properties) and recalculate automatically when dependencies change.
519
+ LazyGraph extends that principle to hierarchical or deeply nested data, making it easier to manage complex relationships than typical flat spreadsheet cells.
520
+ * *Terraform*: Terraform is all about declarative configuration and automatic dependency resolution for infrastructure.
521
+ LazyGraph brings a similar ethos to your application data—you declare how fields depend on one another, and the engine takes care of resolving values in the correct order, even across multiple modules or partial inputs.
522
+ * *JSON Schema*: At its core, LazyGraph consumes standard JSON Schema features (type checks, required fields, etc.) but introduces extended semantics (like derived or default).
523
+ If you’re already comfortable with JSON Schema, you’ll find the basic structure familiar—just expanded to support making your data dynamic and reactive.
524
+ * *Rules Engines* / Expert Systems: Traditional rule engines (like Drools or other Rete-based systems) let you define sets of conditional statements that trigger when facts change.
525
+ In many scenarios, LazyGraph can serve as a lighter-weight alternative: each “rule” (i.e., derived property) is defined right where you need it in the schema, pulling its dependencies from anywhere else in the data. You get transparent, on-demand evaluation without the overhead of an external rules engine. Plus, debug traces show exactly how each value was computed, so there’s no confusion about which rules fired or in which order.
526
+ * *JSON* Path, GraphQL, jq: These tools are great for querying JSON data, but they don't handle automatic lazy dependency resolution.
527
+ LazyGraph’s querying syntax is designed to be expressive and powerful, allowing you to extract the data you need (triggering only the required calculations) from a complex graph of computed values.
528
+
529
+ ### Typical Use Cases
530
+ You can leverage these concepts in a variety of scenarios:
531
+ * Rules Engines: Encode complex, nested and interdepdendent rules into a single, easy to reason about, JSON schema.
532
+ * Complex Chained Computation: Where multiple interdependent properties (e.g. finance calculations, policy checks) must auto-update when a single input changes.
533
+ * Dynamic Configuration: Generate derived settings based on partial overrides without duplicating business logic everywhere.
534
+ * Form or Wizard-Like Data: Reactively compute new fields as users provide partial inputs; only relevant properties are evaluated.
535
+ * Stateless Microservices / APIs: Provide an HTTP endpoint that accepts partial data, automatically fills in defaults and computed fields, and returns a fully resolved JSON object.
536
+
537
+ ## API Reference and Helpers
538
+
539
+ For more fine-grained integration without the DSL or server, you can use the lower-level Ruby API entry points:
540
+
541
+ E.g. where `my_schema` is a LazyGraph compliant JSON Schema:
542
+
543
+ ```ruby
544
+ # 1. Turn the schema hash into a lazy graph. (From which you can generate contexts)
545
+ graph = my_schema.to_lazy_graph() # Optional args debug: false, validate: true
546
+
547
+ # 2. Immediately create a context from the graph and input data.
548
+ ctx = my_schema.to_graph_ctx(input_hash) # Optional args debug: false, validate: true
549
+
550
+ # 3. Immediately evaluate a query on context given the graph and input data.
551
+ result = my_schema.eval!(input_hash, 'some.query') # Optional args debug: false, validate: true
552
+ ```
553
+
554
+ These methods allow you to embed LazyGraph with minimal overhead if you already have your own project structure.
555
+
556
+ ## Contributing
557
+
558
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/wouterken/lazy_graph](https://github.com/wouterken/lazy_graph). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to follow our [code of conduct](./CODE_OF_CONDUCT.md).
559
+
560
+ ## License
561
+
562
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: %i[]
@@ -0,0 +1,117 @@
1
+ require 'vernier'
2
+ require 'memory_profiler'
3
+ require 'benchmark/ips'
4
+ require 'lazy_graph'
5
+
6
+ class PerformanceBuilder < LazyGraph::Builder
7
+ rules_module :performance, {} do
8
+ integer :employees_count, rule: { inputs: 'employees', calc: 'employees.size' }
9
+
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
17
+
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
28
+ end
29
+ end
30
+
31
+ object :positions do
32
+ object :".*", pattern_property: true do
33
+ number :base_rate
34
+ number :salary, default: 100_000
35
+ end
36
+ end
37
+
38
+ object :pay_schedules do
39
+ object :".*", pattern_property: true do
40
+ string :payment_frequency, enum: %w[weekly biweekly semi-monthly monthly],
41
+ description: 'Payment frequency for this pay schedule.'
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def gen_employees(n, m = 10)
48
+ {
49
+ employees: n.times.map do |i|
50
+ {
51
+ id: i.to_s,
52
+ positions: Random.rand(0..4).times.map do
53
+ {
54
+ position_id: Random.rand(1...10).to_s,
55
+ pay_schedule_id: Random.rand(1...10).to_s
56
+ }
57
+ end
58
+ }
59
+ end,
60
+ pay_schedules: [*1..m].map do |i|
61
+ [i, {
62
+ payment_frequency: %w[monthly weekly].sample
63
+ }]
64
+ end.to_h,
65
+ positions: [*1..m].map do |i|
66
+ [i, {
67
+ base_rate: Random.rand(10..100)
68
+ }]
69
+ end.to_h
70
+ }
71
+ end
72
+
73
+ def profile_n(n, debug: false, validate: false)
74
+ employees = gen_employees(n)
75
+ graph = PerformanceBuilder.performance.build!(debug: debug, validate: validate)
76
+ Vernier.profile(out: './examples/time_profile.json') do
77
+ start = Time.now
78
+ graph.context(employees).query('')
79
+ ends = Time.now
80
+ puts "Time elapsed: #{ends - start}"
81
+ end
82
+ end
83
+
84
+ def memory_profile_n(n, debug: false, validate: false)
85
+ employees = gen_employees(n)
86
+ graph = PerformanceBuilder.performance.build!(debug: debug, validate: validate)
87
+ report = MemoryProfiler.report do
88
+ graph.context(employees).query('')
89
+ end
90
+ report.pretty_print
91
+ end
92
+
93
+ def benchmark_ips_n(n, debug: false, validate: false)
94
+ graph = PerformanceBuilder.performance.build!(debug: debug, validate: validate)
95
+ employees = gen_employees(n)
96
+ Benchmark.ips do |x|
97
+ x.report('performance') do
98
+ graph.context(employees).query('')
99
+ end
100
+ x.compare!
101
+ end
102
+ end
103
+
104
+ def console_n(n, debug: false, validate: false)
105
+ require 'debug'
106
+ graph = PerformanceBuilder.performance.build!(debug: debug, validate: validate)
107
+ employees = gen_employees(n)
108
+ result = graph.context(employees).query('')
109
+ binding.b
110
+ end
111
+
112
+ case ARGV[0]
113
+ when 'ips' then benchmark_ips_n(ARGV.fetch(1, 1000).to_i)
114
+ when 'memory' then memory_profile_n(ARGV.fetch(1, 1000).to_i)
115
+ when 'console' then console_n(ARGV.fetch(1, 1000).to_i, debug: true)
116
+ else profile_n(ARGV.fetch(1, 100_000).to_i)
117
+ end