flex-cartesian 1.0 → 1.2

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +156 -104
  4. data/lib/flex-cartesian.rb +15 -1
  5. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b8d43af751cdba0a5c141bab0606576291395a5cc3e3a8cb673bacc86c2920b
4
- data.tar.gz: 96bfd43d1328e48d38665c2c4aa3b951cf24b932c510ab73ed704d7b7bf98c10
3
+ metadata.gz: 7ffe9aa47d7a73dc2f4ab52cc4943d3bdec7ce709a0712b7e92cc4077f785f82
4
+ data.tar.gz: d788a54aee87646c8ab77e58762068dcb4c7d644fdae027365e2ee3446399f8d
5
5
  SHA512:
6
- metadata.gz: 6ff1dbb6deb67e9c9a997d8fb4cea7bb75f8fb4aa9ef8ade0a2452c413e19f97c8687f7816c68d47bef8711fe9cdec98159aae2d80ab468ca55512675f4fd5b3
7
- data.tar.gz: 16764e02f4010d4c6431779f8cb2f9b7e11f6f04b97ac0c7546f5b1431e287936b92207a527a9838ab614aeed635f290e268961c235fbc83553eef4e448c2358
6
+ metadata.gz: be6655fc4acce24a97f2287ee81df181c2a7426fd70b52d0ba02bae3facb80e9720dc5116a9f1dd0afdc0adf412f48b558ee2e5ef154a178a5da5b45dc5e47de
7
+ data.tar.gz: 26210fc67b053bd01946d3ce052c13fe54c035235357ddc7778af98b8487c8485af927d092944dabf92a95c91f040da51bb546617772d6aed2246fe52b079120
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2 - 2025-07-23
4
+ ## Added
5
+ - Optional progress bar to the function command :run
6
+
7
+ ## 1.1 - 2025-07=22
8
+ ### Fixed
9
+ - Dependencies
10
+
3
11
  ## 1.0 - 2025-07-14
4
12
  ### Added
5
13
  - Optional flad for hiding function from output
data/README.md CHANGED
@@ -2,25 +2,23 @@
2
2
 
3
3
  **Ruby implementation of flexible and human-friendly operations on Cartesian products**
4
4
 
5
-
6
-
7
5
  ## Features
8
6
 
9
7
  ✅ Named dimensions with arbitrary keys
10
8
 
11
- ✅ Enumerate over Cartesian product with a single block argument
9
+ ✅ Enumerate over Cartesian space with a single block argument
12
10
 
13
- Functions over Cartesian vectors are decoupled from dimensionality
11
+ Actions on Cartesian are decoupled from dimensionality: `s.cartesian { |v| do_something(v) }`
14
12
 
15
- Define conditions on Cartesian combinations using `s.cond(:set) { |v| v.dim1 > v.dim2 } }` syntax
13
+ Conditions for Cartesian space: `s.cond(:set) { |v| v.dim1 > v.dim2 } }`
16
14
 
17
- Calculate over named dimensions using `s.cartesian { |v| puts "#{v.dim1} and #{v.dim2}" }` syntax
15
+ Calculation over named dimensions: `s.cartesian { |v| puts "#{v.dim1} and #{v.dim2}" }`
18
16
 
19
- Add functions over dimensions using `s.add_function { |v| v.dim1 + v.dim2 }` syntax
17
+ Functions on Cartesian space: `s.func(:add, :my_sum) { |v| v.dim1 + v.dim2 }`
20
18
 
21
19
  ✅ Lazy and eager evaluation
22
20
 
23
- ✅ Progress bars for large Cartesian combinations
21
+ ✅ Progress bars for large Cartesian spaces
24
22
 
25
23
  ✅ Export of Cartesian space to Markdown or CSV
26
24
 
@@ -30,7 +28,77 @@
30
28
 
31
29
  ✅ Structured and colorized terminal output
32
30
 
31
+ ## Use Cases
32
+
33
+ `FlexCartesian` is especially useful in the following scenarios.
34
+
35
+ ### 1. Sweep Analysis of Performance
36
+
37
+ Systematically evaluate an application or algorithm across all combinations of parameters:
38
+
39
+ - Parameters: `threads`, `batch_size`, `backend`, etc
40
+ - Metrics: `throughput`, `latency`, `memory`
41
+ - Output: CSV or Markdown tables
42
+
43
+ ### 2. Hyperparameter Tuning for ML Models
44
+
45
+ Iterate over all combinations of hyperparameters:
46
+
47
+ - Examples: `learning_rate`, `max_depth`, `subsample`, `n_estimators`
48
+ - With constraints (e.g., `max_depth < 10 if learning_rate > 0.1`)
49
+ - With computed evaluation metrics like `accuracy`, `AUC`, etc
50
+
51
+ ### 3. Infrastructure and System Configuration
52
+
53
+ Generate all valid infrastructure configurations:
54
+
55
+ ```ruby
56
+ region: ["us-west", "eu-central"]
57
+ tier: ["basic", "pro"]
58
+ replicas: [1, 3, 5]
59
+ ```
60
+
61
+ With conditions like "basic tier cannot have more than one replica:
62
+ ```ruby
63
+ s.cond(:set) { |v| (v.tier == "basic" ? v.replicas == 1 : true) }
64
+ ```
65
+
66
+ ### 4. Mass Testing of CLI Commands
67
+ Generate and benchmark all valid CLI calls:
68
+
69
+ ```bash
70
+ myapp --threads=4 --batch=32 --backend=torch
71
+ ```
72
+
73
+ Capture runtime, output, errors, etc.
74
+
75
+ ### 5. Input Generation for UI/API Testing
76
+ Automatically cover input parameter spaces for:
77
+
78
+ - HTTP methods: ["GET", "POST"]
79
+ - User roles: ["guest", "user", "admin"]
80
+ - Language settings: ["en", "fr", "de"]
33
81
 
82
+ ### 6. Scientific and Engineering Simulations
83
+ Generate multidimensional experimental spaces for:
84
+
85
+ - Physics simulations
86
+ - Bioinformatics parameter sweeps
87
+ - Network behavior modeling, etc
88
+
89
+ ### 7. Structured Reporting and Visualization
90
+ Output Cartesian data as:
91
+
92
+ - Markdown (for GitHub rendering)
93
+ - CSV (for Excel, Google Sheets, and more advanced BI tools)
94
+ - Plain text (for CLI previews)
95
+
96
+ ### 8. Test Case Generation
97
+ Use it to drive automated test inputs for:
98
+
99
+ - RSpec shared examples
100
+ - Minitest table-driven tests
101
+ - PyTest parameterization
34
102
 
35
103
  ## Installation
36
104
 
@@ -98,25 +166,24 @@ s.cartesian(lazy: true).take(2).each { |v| do_something(v) }
98
166
 
99
167
  # 5. A function is a virtual dimension that is calculated based on a vector of base dimensions.
100
168
  # You can think of a function as a scalar field defined on Cartesian space.
101
- # 6. Functions are printed as virtual dimensions in .output method.
102
- # 7. However, functions remains virtual construct, and their values can't be referenced by name
103
- # (unlike regular dimensions). Also, functions do not add to .size of Cartesian space.
169
+ # 6. Functions are printed as virtual dimensions in `.output`.
170
+ # 7. Functions do not add to `.size` of Cartesian space.
104
171
 
105
172
  puts "\nAdd function 'triple'"
106
173
  puts "Note: function is visualized in .output as a new dimension"
107
- s.add_function(:triple) { |v| v.dim1 * 3 + (v.dim3 ? 1: 0) }
108
- # Note: however, function remains a virtual construct, and it cannot be referenced by name
174
+ s.func(:add, :triple) { |v| v.dim1 * 3 + (v.dim3 ? 1: 0) }
175
+ s.func(:run)
109
176
  s.output
110
177
 
111
178
  puts "\Add and then remove function 'test'"
112
- s.add_function(:test) { |v| v.dim3.to_i }
113
- s.remove_function(:test)
179
+ s.func(:add, :test) { |v| v.dim3.to_i }
180
+ s.func(:del, :test)
114
181
 
115
182
 
116
183
 
117
184
  # CONDITIONS ON CARTESIAN SPACE
118
185
 
119
- # 8. A condition is a logical restriction of allowed combitnations for Cartesian space.
186
+ # 8. A condition is a logical constraint for allowed combitnations of Cartesian space.
120
187
  # 9. Using conditions, you can take a slice of Cartesian space.
121
188
  # In particular, you can reflect semantical dependency of dimensional values.
122
189
 
@@ -140,10 +207,8 @@ puts "Restored size without conditions: #{s.size}"
140
207
 
141
208
  puts "\nPrint Cartesian space as plain table, all functions included"
142
209
  s.output
143
-
144
210
  puts "\nPrint Cartesian space as Markdown"
145
211
  s.output(format: :markdown)
146
-
147
212
  puts "\nPrint Cartesian space as CSV"
148
213
  s.output(format: :csv)
149
214
 
@@ -155,7 +220,6 @@ puts "\nImport Cartesian space from JSON (similar method for YAML)"
155
220
  File.write('example.json', JSON.pretty_generate(example))
156
221
  puts "\nNote: after import, all assigned functions will calculate again, and they appear in the output"
157
222
  s.import('example.json').output
158
-
159
223
  puts "\nExport Cartesian space to YAML (similar method for JSON)"
160
224
  s.export('example.yaml', format: :yaml)
161
225
 
@@ -166,7 +230,6 @@ s.export('example.yaml', format: :yaml)
166
230
  puts "\nGet number of Cartesian combinations"
167
231
  puts "Note: .size counts only dimensions, it ignores virtual constructs (functions, conditions, etc.)"
168
232
  puts "Total size of Cartesian space: #{s.size}"
169
-
170
233
  puts "\nPartially converting Cartesian space to array:"
171
234
  array = s.to_a(limit: 3)
172
235
  puts array.inspect
@@ -218,62 +281,40 @@ These input parameters form the following dimensions.
218
281
  }
219
282
  ```
220
283
 
221
- Note that '//' isn't officially supported by JSON, and you may want to remove the comments if you experience parser errors.
284
+ Note that `//` isn't officially supported by JSON, and you may want to remove the comments if you experience parser errors.
222
285
  Let us build the code to run over these parameters.
223
286
 
224
287
  ```ruby
225
288
  require 'flex-cartesian'
226
289
 
227
- s = FlexCartesian.new(path: './ping_config.json') # file with the parameters as given above
290
+ s = FlexCartesian.new(path: './ping_parameters.json') # file with the parameters as given above
228
291
 
229
- result = {} # here we will store raw result of each ping and fetch target metrics from it
230
-
231
- # this function shows actual ping command
232
- s.func(:add, :command) do |v|
233
- "ping -c #{v.count} -s #{v.size} #{v.target}"
234
- end
292
+ result = {} # raw result of each ping
235
293
 
236
- # this function gets raw result of actual ping command
237
- s.func(:add, :raw_ping, hide: true) do |v|
238
- result[v.command] ||= `#{v.command} 2>&1`
239
- end
294
+ s.func(:add, :command) { |v| "ping -c #{v.count} -s #{v.size} #{v.target}" } # ping command
295
+ s.func(:add, :raw_ping, hide: true) { |v| result[v.command] ||= `#{v.command} 2>&1` } # capturing ping result
296
+ s.func(:add, :time) { |v| v.raw_ping[/min\/avg\/max\/(?:mdev|stddev) = [^\/]+\/([^\/]+)/, 1]&.to_f } # fetch ping time from result
297
+ s.func(:add, :min) { |v| v.raw_ping[/min\/avg\/max\/(?:mdev|stddev) = ([^\/]+)/, 1]&.to_f } # fetch min time from result
298
+ s.func(:add, :loss) { |v| v.raw_ping[/(\d+(?:\.\d+)?)% packet loss/, 1]&.to_f } # fetch ping loss from result
240
299
 
241
- # this function extracts ping time
242
- s.func(:add, :time) do |v|
243
- if v.raw_ping =~ /min\/avg\/max\/(?:mdev|stddev) = [^\/]+\/([^\/]+)/
244
- $1.to_f
245
- end
246
- end
300
+ s.func(:run, progress: true, title: "Pinging") # Sweep analysis! Benchmark all possible combinations of parameters
247
301
 
248
- # this function extracts minimum ping time
249
- s.func(:add, :min) do |v|
250
- if v.raw_ping =~ /min\/avg\/max\/(?:mdev|stddev) = ([^\/]+)/
251
- $1.to_f
252
- end
253
- end
302
+ s.output(format: :csv, file: './result.csv') # save benchmark result as CSV
254
303
 
255
- # funally, this function extracts losses of ping
256
- s.func(:add, :loss) do |v|
257
- if v.raw_ping =~ /(\d+(?:\.\d+)?)% packet loss/
258
- $1.to_f
259
- end
260
- end
304
+ s.output(colorize: true) # for convenience, show result in terminal
305
+ ```
261
306
 
262
- # this is the spinal axis of FlexCartesian:
263
- # calculate all functions on the entire Cartesian space of parameters aka dimensions
264
- s.func(:run)
307
+ If you run the code, after a while it will generate benchmark results on the screen:
265
308
 
266
- # save benchmark results to CSV for convenient analysis in BI tools
267
- s.output(format: :csv, file: './benchmark.csv')
309
+ ![Ping Benchmark Example](doc/ping_benchmark_example.png)
268
310
 
269
- # for convenience, show tabular result on screen as well
270
- s.output(colorize: true)
271
- ```
311
+ Additionally, CSV version of this result is saved as `./benchmark.csv`
272
312
 
273
- This code is 100% practical and illustrative. You can benchmark:
313
+ The PING benchmarking code above is 100% practical and illustrative.
314
+ You can modify it and benchmark virtually anything:
274
315
 
275
- - Local block devices using 'dd'
276
- - GPU-to-Storage connection using 'gdsio'
316
+ - Local block devices using `dd`
317
+ - GPU-to-Storage connection using `gdsio`
277
318
  - Local file systems using FS-based utilities
278
319
  - Local CPU RAM using RAM disk or specialized benchmarks for CPU RAM
279
320
  - Database performance using SQL client or non-SQL client utilities
@@ -285,15 +326,19 @@ In any use case, FlexCartesian will unfold complete landscape of the target perf
285
326
  As result, you will be able to spot optimal configurations, correlations, bottlenecks, and sweet spots.
286
327
  Moreover, you will make your conclusions in a justifiable way.
287
328
 
288
- Here is an example of how I used FlexCartesian to [analyze optimal performance/cost of YOLO](https://www.linkedin.com/pulse/comparing-gpu-a10-ampere-a1-shapes-object-oci-yuri-rassokhin-rseqf).
329
+ Here is example of using FlexCartesian for [performance/cost analysis of YOLO](https://www.linkedin.com/pulse/comparing-gpu-a10-ampere-a1-shapes-object-oci-yuri-rassokhin-rseqf).
330
+
331
+
289
332
 
290
333
  ## API Overview
291
334
 
292
335
  ### Initialization
293
336
  ```ruby
294
- FlexCartesian.new(dimensions_hash)
337
+ FlexCartesian.new(dimensions = nil, path: nil, format: :json)
295
338
  ```
296
- - `dimensions_hash`: a hash with named dimensions; each value can be an `Enumerable` (e.g., arrays, ranges).
339
+ - `dimensions_hash`: optional hash with named dimensions; each value can be an `Enumerable` (arrays, ranges, etc)
340
+ - `path`: optional path to file with stored dimensions, JSON and YAML supported
341
+ - `format`: optional format of `path` file, defaults to JSON
297
342
 
298
343
  Example:
299
344
  ```ruby
@@ -309,10 +354,11 @@ FlexCartesian.new(dimensions)
309
354
  ---
310
355
 
311
356
  ### Iterate Over All Combinations
357
+
358
+ Example:
312
359
  ```ruby
313
360
  # With block
314
361
  cartesian(dims = nil, lazy: false) { |vector| ... }
315
-
316
362
  # Without block: returns Enumerator
317
363
  cartesian(dims = nil, lazy: false)
318
364
  ```
@@ -326,20 +372,33 @@ s.cartesian { |v| puts "#{v.dim1} - #{v.dim2}" }
326
372
 
327
373
  ---
328
374
 
329
- ### Add / Remove Functions
375
+ ### Handling Functions
330
376
  ```ruby
331
- add_function(name, &block)
332
- remove_function(name)
377
+ func(command = :print, name = nil, hide: false, progress: false, title: "calculating functions", &block)
333
378
  ```
334
- - `name`: symbol the name of the virtual dimension (e.g. `:label`)
379
+ - `command`: symbol, one of the following
380
+ - `:add` to add function as a virtual dimension to Cartesian space
381
+ - `:del` to delete function from Cartesian space
382
+ - `:print` as defaut action, prints all the functions added to Cartesian space
383
+ - `:run` to calculate all the functions defined for Cartesian space
384
+ - `name`: symbol, name of the virtual dimension, e.g. `:my_function`
385
+ - `hide`: flag that hides or shows the function in .output; it is useful to hide intermediate calculations
386
+ - `progress`: show progress bar during `:run`, useful for large Cartesian space
387
+ - `title`: title of the progress bar
335
388
  - `block`: a function that receives each vector and returns a computed value
336
389
 
337
390
  Functions show up in `.output` like additional (virtual) dimensions.
338
391
 
392
+ > Note: functions must be calculated excpliticy using `:run` command.
393
+ > Before the first calculation, a function has `nil` values in `.output`.
394
+ > Explicit :run is reequired to unambigously control points in the execution flow where high computational resource is to be consumed.
395
+ > Otherwise, automated recalculation of functions, perhaps, during `.output` would be a difficult-to-track computational burden.
396
+
339
397
  Example:
340
398
  ```ruby
341
399
  s = FlexCartesian.new( { dim1: [1, 2], dim2: ['A', 'B'] } )
342
- s.add_function(:increment) { |v| v.dim1 + 1 }
400
+ s.func(:add, :increment) { |v| v.dim1 + 1 }
401
+ s.func(:run)
343
402
 
344
403
  s.output(format: :markdown)
345
404
  # | dim1 | dim2 | increment |
@@ -349,7 +408,6 @@ s.output(format: :markdown)
349
408
  # ...
350
409
  ```
351
410
 
352
- > Note: functions are virtual — they are not part of the base dimensions, but they integrate seamlessly in output.
353
411
 
354
412
  ---
355
413
 
@@ -377,19 +435,20 @@ Displays a progress bar using `ruby-progressbar`.
377
435
 
378
436
  ---
379
437
 
380
- ### Print Table to Console
438
+ ### Print Cartesian
381
439
  ```ruby
382
- output(
383
- separator: " | ",
384
- colorize: false,
385
- align: false,
386
- format: :plain # or :markdown, :csv
387
- limit: nil
388
- )
440
+ output(separator: " | ", colorize: false, align: true, format: :plain, limit: nil, file: nil)
389
441
  ```
442
+ - `separator`: how to visually separate columns in the output
443
+ - `colorize`: whether to colorize output or not
444
+ - `align`: whether to align output by column or not
445
+ - `format`: one of `:plain`, `:markdown`, or `:csv`
446
+ - `limit`: break the output after the first `limit` Cartesian combinations
447
+ - `file`: print to `file`
448
+
390
449
  Prints all combinations in table form (plain/markdown/CSV).
391
450
  Markdown example:
392
- ```
451
+ ```markdown
393
452
  | dim1 | dim2 |
394
453
  |------|------|
395
454
  | 1 | "a" |
@@ -400,9 +459,10 @@ Markdown example:
400
459
 
401
460
  ### Import from JSON or YAML
402
461
  ```ruby
403
- import('file.json',
404
- format: :json) # or :yaml
462
+ import(path, format: :json)
405
463
  ```
464
+ - `path`: input file
465
+ - `format`: format to read, `:json` and `:yaml` supported
406
466
 
407
467
  Obsolete import methods:
408
468
  ```ruby
@@ -412,38 +472,30 @@ s.from_yaml("file.yaml")
412
472
 
413
473
  ---
414
474
 
415
- ### Export from JSON or YAML
416
- ```ruby
417
- export('file.json',
418
- format: :json) # or :yaml
419
- ```
420
-
421
- ---
422
-
423
- ### Print Cartesian Space
424
- Each yielded combination is a `Struct` extended with:
425
- ```ruby
426
- output(separator: " | ", colorize: false, align: true)
427
- ```
428
- Example:
475
+ ### Export to JSON or YAML
429
476
  ```ruby
430
- s.cartesian { |v| v.output(colorize: true, align: false) }
477
+ export(path, format: :json)
431
478
  ```
432
-
433
- ---
479
+ - `path`: output file
480
+ - `format`: format to export, `:json` and `:yaml` supported
434
481
 
435
482
  ### Conditions on Cartesian Space
436
483
  ```ruby
437
- cond(command = :print, # or :set, :unset, :clear
438
- index: nil, # index of a conditions to unset
439
- &block # defintiion of the condition to set
440
- )
484
+ cond(command = :print, index: nil, &block)
441
485
  ```
486
+ - `command`: one of the following
487
+ - `:set` to set the condition to Cartesian space
488
+ - `:unset` to remove the `index` condition from Cartesian space
489
+ - `:clear` to remove all conditions from Cartesian space
490
+ - `:print` default command, prints all the conditions on the Cartesian space
491
+ - `index`: index of the condition set to Cartesian space, it is used to remove specified condition
492
+ - `block`: definition of the condition, it should return `true` or `false` to avoid unpredictable behavior
493
+
442
494
  Example:
443
495
  ```ruby
444
496
  s.cond(:set) { |v| v.dim1 > v.dim3 }
445
497
  s.cond # defaults to s.cond(:print) and shows all the conditions in the form 'index | definition'
446
- s.cond(:unset, 0) # remove previously set condition
498
+ s.cond(:unset, 0) # remove the condition
447
499
  s.cond(:clear) # remove all conditions, if any
448
500
  ```
449
501
 
@@ -59,7 +59,7 @@ class FlexCartesian
59
59
  end
60
60
  end
61
61
 
62
- def func(command = :print, name = nil, hide: false, &block)
62
+ def func(command = :print, name = nil, hide: false, progress: false, title: "calculating functions", &block)
63
63
  case command
64
64
  when :add
65
65
  raise ArgumentError, "Function name and block required for :add" unless name && block_given?
@@ -86,12 +86,26 @@ def func(command = :print, name = nil, hide: false, &block)
86
86
 
87
87
  when :run
88
88
  @function_results = {}
89
+
90
+ if progress
91
+ bar = ProgressBar.create(title: title, total: size, format: '%t [%B] %p%% %e')
92
+
93
+ cartesian do |v|
94
+ @function_results[v] ||= {}
95
+ @derived.each do |fname, block|
96
+ @function_results[v][fname] = block.call(v)
97
+ end
98
+ bar.increment if progress
99
+ end
100
+
101
+ else
89
102
  cartesian do |v|
90
103
  @function_results[v] ||= {}
91
104
  @derived.each do |fname, block|
92
105
  @function_results[v][fname] = block.call(v)
93
106
  end
94
107
  end
108
+ end
95
109
 
96
110
  else
97
111
  raise ArgumentError, "Unknown command for function: #{command.inspect}"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flex-cartesian
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.0'
4
+ version: '1.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yury Rassokhin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-14 00:00:00.000000000 Z
11
+ date: 2025-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: progressbar
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.13'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.13'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: json
43
57
  requirement: !ruby/object:Gem::Requirement