lab_tech 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +323 -0
- data/Rakefile +30 -0
- data/app/models/lab_tech/application_record.rb +5 -0
- data/app/models/lab_tech/default_cleaner.rb +87 -0
- data/app/models/lab_tech/experiment.rb +190 -0
- data/app/models/lab_tech/observation.rb +40 -0
- data/app/models/lab_tech/percentile.rb +41 -0
- data/app/models/lab_tech/result.rb +130 -0
- data/app/models/lab_tech/speedup.rb +65 -0
- data/app/models/lab_tech/summary.rb +183 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20190815192130_create_experiment_tables.rb +50 -0
- data/lib/lab_tech.rb +176 -0
- data/lib/lab_tech/engine.rb +6 -0
- data/lib/lab_tech/version.rb +3 -0
- data/lib/tasks/lab_tech_tasks.rake +4 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +1 -0
- data/spec/dummy/app/assets/javascripts/application.js +14 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/bin/update +28 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +35 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +46 -0
- data/spec/dummy/config/environments/production.rb +71 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cors.rb +16 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +34 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/db/schema.rb +52 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -0
- data/spec/dummy/log/test.log +1519 -0
- data/spec/examples.txt +79 -0
- data/spec/models/lab_tech/default_cleaner_spec.rb +32 -0
- data/spec/models/lab_tech/experiment_spec.rb +110 -0
- data/spec/models/lab_tech/percentile_spec.rb +85 -0
- data/spec/models/lab_tech/result_spec.rb +198 -0
- data/spec/models/lab_tech/speedup_spec.rb +133 -0
- data/spec/models/lab_tech/summary_spec.rb +325 -0
- data/spec/models/lab_tech_spec.rb +23 -0
- data/spec/rails_helper.rb +62 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/misc_helpers.rb +7 -0
- metadata +238 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2f5e6f3f09101904d4cf6fbc209768dba3f58bdb3ca9bbe157473e7d939fe124
|
4
|
+
data.tar.gz: 88dceb210ec735257a5f2cf329fe132085062b39dd2ad207dae2d0ff16392993
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b52697112c48077ac53e85af052b0e20e5425f448fba77a8139344afad7bb39c4bf497dcf5987d794d5826e5a6dfd920975a0d509d44fb16779d92342d21ffbd
|
7
|
+
data.tar.gz: d6178294c5066e7117435237b7ba2a93a1bbf15bba7152ead2c6f4393a808c0af1d271478126e50a5948ee133c855fa28ca2589da140a305cc5698271ce0fd81
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Sam Livingston-Gray
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
# LabTech!
|
2
|
+
Rails engine for using GitHub's 'Scientist' library with ActiveRecord, for those of us not operating apps at ROFLscale
|
3
|
+
|
4
|
+
Please go read [Scientist's amazing
|
5
|
+
README](https://github.com/github/scientist/blob/master/README.md). This tool
|
6
|
+
won't make any sense until you understand what Scientist is for and how it
|
7
|
+
works.
|
8
|
+
|
9
|
+
If conference videos are your thing, Jesse Toth's ["Easy Rewrites With Ruby and
|
10
|
+
Science!"](http://www.confreaks.tv/videos/rubyconf2014-easy-rewrites-with-ruby-and-science)
|
11
|
+
from RubyConf 2014 is well worth your time as well.
|
12
|
+
|
13
|
+
**NOTE: our examples assume that you have access to the Rails production
|
14
|
+
console.** If you work at a company that locks this down, you'll need to write
|
15
|
+
an administrative back-end UI to enable and disable experiments and review them
|
16
|
+
for accuracy and performance. (Please feel free to send those back to us in a
|
17
|
+
pull request; we simply haven't needed them for ourselves, so they don't
|
18
|
+
exist yet.)
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Once you've installed the gem and run its migrations (as described in
|
23
|
+
"Installation", below), you can start running experiments.
|
24
|
+
|
25
|
+
For the purposes of this README, let's say we have a Customer Relationship
|
26
|
+
Manager (CRM) that lets its users search for leads using some predefined set of
|
27
|
+
criteria (name, location, favorite food, whatever). (Any resemblance to an
|
28
|
+
actual SaaS product we sell here at Real Geeks is... purely coincidental.)
|
29
|
+
|
30
|
+
Let's say, too, that the code behind that search started out sort of okay, but
|
31
|
+
it got worse and worse over time until someone decided it was time for a full
|
32
|
+
rewrite. The old search code lives in a method named `Lead.search`, but we've
|
33
|
+
been working on a replacement that lives in `SpiffySearch.leads`. The tests
|
34
|
+
for SpiffySearch are in great shape, and we're confident that we won't be
|
35
|
+
causing any 500 errors -- but we'd like to use a tool like Scientist to make
|
36
|
+
sure that SpiffySearch returns the same results in the same order our users
|
37
|
+
expect.
|
38
|
+
|
39
|
+
Stand back -- we're going to try SCIENCE!
|
40
|
+
|
41
|
+
The first thing we need is a name for our experiment -- let's just go with
|
42
|
+
`"spiffy-search"`.
|
43
|
+
|
44
|
+
### Deploying an Experiment
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
LabTech.science "spiffy-search" do |exp|
|
48
|
+
exp.use { Lead.search(params[:search]) } # control
|
49
|
+
exp.try { SpiffySearch.leads(params[:search]) } # candidate
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Within the block, `exp` is an object that includes the `Scientist::Experiment`
|
54
|
+
module, so we can use any and all of the tools in the Scientist README.
|
55
|
+
|
56
|
+
However, I want to call out a few specific ones as being extremely useful for
|
57
|
+
this sort of thing. When working with code that returns interesting objects,
|
58
|
+
it's a Very Good Idea™ to make use of the `clean` method on the experiment.
|
59
|
+
It's probably also good to override the default comparison and to provide some
|
60
|
+
context for the experiment, so let's just redo that block with that in mind:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
LabTech.science "spiffy-search" do |exp|
|
64
|
+
exp.context params: params.to_h
|
65
|
+
|
66
|
+
exp.use { Lead.search(params[:search]) } # control
|
67
|
+
exp.try { SpiffySearch.leads(params[:search]) } # candidate
|
68
|
+
|
69
|
+
exp.compare {|control, candidate| control.map(&:id) == candidate.map(&:id) }
|
70
|
+
exp.clean { |records| records.map(&:id) }
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
Now that that's done, we can safely commit and deploy this code. As soon as
|
75
|
+
that code starts getting run, we'll see a new LabTech::Experiment record in the
|
76
|
+
database. However, **the candidate block will never run,** because
|
77
|
+
LabTech::Experiment records are disabled by default.
|
78
|
+
|
79
|
+
### Enabling an Experiment
|
80
|
+
|
81
|
+
In order to enable the experiment, we'll need to go into the Rails production
|
82
|
+
console and run:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
LabTech.enable "spiffy-search"
|
86
|
+
```
|
87
|
+
|
88
|
+
If we have particularly high search volume and we only want to run the
|
89
|
+
experiment on a fraction of our requests, the `.enable` method takes an
|
90
|
+
optional `:percent` keyword argument, which accepts an integer in the range
|
91
|
+
`(0..100)`. So to sample only, say, 3% of our searches (selected at random),
|
92
|
+
we could run this instead:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
LabTech.enable "spiffy-search", percent: 3
|
96
|
+
```
|
97
|
+
|
98
|
+
### Summarizing Experimental Results
|
99
|
+
|
100
|
+
At this point, if you have the [table_print gem](http://tableprintgem.com/)
|
101
|
+
installed, you can get a quick overview of all of your experiments by running:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
tp LabTech::Experiment.all
|
105
|
+
```
|
106
|
+
|
107
|
+
Either way, if you want more details on how the experiment is running, you can run:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
tp LabTech.summarize_results "spiffy-search"
|
111
|
+
```
|
112
|
+
|
113
|
+
This will print a terminal-friendly summary of your experimental results. I'll
|
114
|
+
talk more about this summary later, but for now, let's say we were
|
115
|
+
overconfident, and there's a bug in SpiffySearch that's raising an exception.
|
116
|
+
The summary will have a line that looks like this:
|
117
|
+
|
118
|
+
```
|
119
|
+
22 of 22 (100.00%) raised errors
|
120
|
+
```
|
121
|
+
|
122
|
+
Ruh roh!
|
123
|
+
|
124
|
+
### Summarizing Errors
|
125
|
+
|
126
|
+
We run this to see what's up:
|
127
|
+
|
128
|
+
```
|
129
|
+
LabTech.summarize_errors "spiffy-search"
|
130
|
+
```
|
131
|
+
|
132
|
+
And we see something that looks like this, only longer:
|
133
|
+
|
134
|
+
```
|
135
|
+
====================================================================================================
|
136
|
+
Comparing results for smoke-test:
|
137
|
+
|
138
|
+
|
139
|
+
----------------------------------------------------------------------------------------------------
|
140
|
+
Result #1
|
141
|
+
* RuntimeError: nope
|
142
|
+
----------------------------------------------------------------------------------------------------
|
143
|
+
----------------------------------------------------------------------------------------------------
|
144
|
+
Result #2
|
145
|
+
* RuntimeError: nope
|
146
|
+
----------------------------------------------------------------------------------------------------
|
147
|
+
|
148
|
+
====================================================================================================
|
149
|
+
```
|
150
|
+
|
151
|
+
If you want to see individual backtraces, you can do so by finding and
|
152
|
+
inspecting indvididual records in the Rails console. For now, though, let's
|
153
|
+
say we know where the error is...
|
154
|
+
|
155
|
+
### Disabling and Restarting an Experiment
|
156
|
+
|
157
|
+
There's no point continuing to collect those exceptions, so we might as well
|
158
|
+
turn the experiment back off:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
LabTech.disable "spiffy-search"
|
162
|
+
```
|
163
|
+
|
164
|
+
We fix the exception, deploy the new code, and now we want to start the
|
165
|
+
experiment over again. We don't want the previous exceptions cluttering up our
|
166
|
+
new results, so let's clear out all those observations:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
exp = LabTech::Experiment.named("spiffy-search")
|
170
|
+
exp.purge_data
|
171
|
+
```
|
172
|
+
|
173
|
+
(Yes, this is a slightly more cumbersome interface than enabling or summarizing
|
174
|
+
an experiment. While deleting data is sometimes necessary, we don't want to
|
175
|
+
make it easy to do accidentally.)
|
176
|
+
|
177
|
+
### Summarizing Experimental Results, Take Two
|
178
|
+
|
179
|
+
This time, the output from `LabTech.summarize_results "spiffy-search"` looks
|
180
|
+
more like this:
|
181
|
+
|
182
|
+
```
|
183
|
+
--------------------------------------------------------------------------------
|
184
|
+
Experiment: smoke-test
|
185
|
+
--------------------------------------------------------------------------------
|
186
|
+
Earliest results: 2019-08-21T11:00:41-10:00
|
187
|
+
Latest result: 2019-08-21T11:23:31-10:00 (23 minutes)
|
188
|
+
|
189
|
+
103 of 106 (97.16%) correct
|
190
|
+
2 of 106 (1.88%) mismatched
|
191
|
+
1 of 106 (0.94%) timed out
|
192
|
+
|
193
|
+
Median time delta: +0.000s (90% of observations between -0.000s and +0.000s)
|
194
|
+
|
195
|
+
Speedups (by percentiles):
|
196
|
+
0% [ █ · ] -3.1x
|
197
|
+
5% [ █ · ] -2.8x
|
198
|
+
10% [ █ · ] -2.6x
|
199
|
+
15% [ █ · ] -2.5x
|
200
|
+
20% [ █ · ] -2.4x
|
201
|
+
25% [ █ · ] -2.3x
|
202
|
+
30% [ █ · ] -2.2x
|
203
|
+
35% [ █ · ] -2.1x
|
204
|
+
40% [ █ · ] -2.0x
|
205
|
+
45% [ · █ ] +1.2x faster
|
206
|
+
50% [ · · · · · · · · · · · · · · · · █ · · · · · · · · ] +1.8x faster
|
207
|
+
55% [ · █ ] +2.0x faster
|
208
|
+
60% [ · █ ] +2.1x faster
|
209
|
+
65% [ · █ ] +2.2x faster
|
210
|
+
70% [ · █ ] +2.4x faster
|
211
|
+
75% [ · █ ] +2.6x faster
|
212
|
+
80% [ · █ ] +2.6x faster
|
213
|
+
85% [ · █ ] +2.7x faster
|
214
|
+
90% [ · █ ] +2.8x faster
|
215
|
+
95% [ · █ ] +3.0x faster
|
216
|
+
100% [ · █] +6.7x faster
|
217
|
+
--------------------------------------------------------------------------------
|
218
|
+
```
|
219
|
+
|
220
|
+
First off, we see a summary of the time range represented in this experiment.
|
221
|
+
This is a very simple "first result to last result" view that does not take
|
222
|
+
into account when the experiment was enabled.
|
223
|
+
|
224
|
+
Next, we see some counts. An individual run of an experiment may have one of
|
225
|
+
four outcomes:
|
226
|
+
- "correct" means that both control and candidate were considered equivalent
|
227
|
+
- "mismatched" means that the candidate returned a different value than the
|
228
|
+
control
|
229
|
+
- "timed out" means that the experiment's run raised a `Timeout::Error`
|
230
|
+
- "raised error" means that the experiment's run raised anything other than
|
231
|
+
`Timeout::Error`
|
232
|
+
|
233
|
+
After the counts, we see a bunch of performance data, starting with a line that
|
234
|
+
says "Median time delta" and includes the 5th and 95th percentile time deltas
|
235
|
+
as well. "Time delta" just means the difference in execution time between the
|
236
|
+
control and the candidate: negative values are faster, and positive values are
|
237
|
+
slower. (The 5th and 95th percentiles are deliberately chosen to keep us from
|
238
|
+
worrying too much about extreme values that might be outliers.)
|
239
|
+
|
240
|
+
The rest of the output is taken up by a chart that attempts to provide a handy
|
241
|
+
visual chart showing whether the candidate is faster or slower than the
|
242
|
+
control. Because it can be hard to remember what the signs signify, this also
|
243
|
+
includes the word "faster" when the candidate was faster than the control.
|
244
|
+
|
245
|
+
### Comparing Mismatches
|
246
|
+
|
247
|
+
At this point, we might be curious about any mismatches, and want to
|
248
|
+
investigate those. Unfortunately, the chart I showed above was edited by hand
|
249
|
+
to show what the output might look like if mismatches were present, but as of
|
250
|
+
this writing I don't actually have any mismatches to show you. (I promise
|
251
|
+
that's not a humblebrag.)
|
252
|
+
|
253
|
+
However, you can get a quick, if EXTREMELY VERBOSE, listing of the first few
|
254
|
+
mismatches by running:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
LabTech.compare_mismatches "spiffy-search", limit: 3
|
258
|
+
```
|
259
|
+
|
260
|
+
You have the ability to customize the output of this by passing a block that
|
261
|
+
takes a "control" parameter followed by a "candidate" parameter; the return
|
262
|
+
value of that block will be printed to the console. How you do this will
|
263
|
+
largely depend on the kind of data you're collecting to validate your
|
264
|
+
experiments. There are several examples in the `lib/lab_tech.rb` file; I
|
265
|
+
encourage you to check them out.
|
266
|
+
|
267
|
+
## Installation
|
268
|
+
|
269
|
+
**NOTE: As this gem is a Rails engine, we assume you have a Rails application to
|
270
|
+
include it in.**
|
271
|
+
|
272
|
+
Add this line to your application's Gemfile:
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
gem 'lab_tech'
|
276
|
+
```
|
277
|
+
|
278
|
+
And then execute:
|
279
|
+
```bash
|
280
|
+
$ bundle
|
281
|
+
```
|
282
|
+
|
283
|
+
Or install it yourself as:
|
284
|
+
```bash
|
285
|
+
$ gem install lab_tech
|
286
|
+
```
|
287
|
+
|
288
|
+
Once the gem is installed, run this from your application's root (possibly with
|
289
|
+
the `bundle exec` or `bin/` prefix, or whatever else may be dictated by your
|
290
|
+
local custom and practice):
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
rails lab_tech:install:migrations db:migrate
|
294
|
+
```
|
295
|
+
|
296
|
+
The output from that command should look like this:
|
297
|
+
|
298
|
+
```
|
299
|
+
Copied migration 20190822175815_create_experiment_tables.lab_tech.rb from lab_tech
|
300
|
+
== 20190822175815 CreateExperimentTables: migrating ===========================
|
301
|
+
-- create_table("lab_tech_experiments")
|
302
|
+
-> 0.0147s
|
303
|
+
-- create_table("lab_tech_results")
|
304
|
+
-> 0.0152s
|
305
|
+
-- create_table("lab_tech_observations")
|
306
|
+
-> 0.0109s
|
307
|
+
== 20190822175815 CreateExperimentTables: migrated (0.0410s) ==================
|
308
|
+
```
|
309
|
+
|
310
|
+
Once that's done, you should be good to go! See the "Usage" section, above.
|
311
|
+
|
312
|
+
## Contributing
|
313
|
+
|
314
|
+
This gem was extracted just before its primary author left Real Geeks, so it's
|
315
|
+
not quite clear who's going to take responsibility for the gem. It's probably
|
316
|
+
a good idea to open a GitHub issue to start a conversation before undertaking
|
317
|
+
any great amount of work -- though, of course, you're perfectly welcome to fork
|
318
|
+
the gem and use your modified version at any time.
|
319
|
+
|
320
|
+
## License
|
321
|
+
|
322
|
+
The gem is available as open source under the terms of the [MIT
|
323
|
+
License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'LabTech'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rspec/core'
|
25
|
+
require 'rspec/core/rake_task'
|
26
|
+
|
27
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
28
|
+
RSpec::Core::RakeTask.new(spec: "app:db:test:prepare")
|
29
|
+
|
30
|
+
task default: :spec
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module LabTech
|
2
|
+
class DefaultCleaner
|
3
|
+
def self.call( value )
|
4
|
+
new.call( value )
|
5
|
+
end
|
6
|
+
|
7
|
+
def call( value )
|
8
|
+
clean( value, return_placeholders: false )
|
9
|
+
end
|
10
|
+
|
11
|
+
class RecordPlaceholder
|
12
|
+
attr_reader :class_name, :id
|
13
|
+
def initialize( record )
|
14
|
+
@class_name = record.class.to_s
|
15
|
+
@id = record.id
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_a
|
19
|
+
[ class_name, id ]
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
"<#{class_name} ##{id}>"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# In the event of a Recursion Blunder, stop before we smash the stack, so we
|
30
|
+
# can actually get a useful stack trace that doesn't overwhelm our scrollback
|
31
|
+
# buffers
|
32
|
+
MAX_DEPTH = 10
|
33
|
+
def __push__
|
34
|
+
@depth ||= 0
|
35
|
+
@depth += 1
|
36
|
+
fail "wtf are you even doing?" if @depth > MAX_DEPTH
|
37
|
+
end
|
38
|
+
def __pop__
|
39
|
+
@depth -= 1
|
40
|
+
end
|
41
|
+
|
42
|
+
def clean( value, return_placeholders: )
|
43
|
+
__push__
|
44
|
+
|
45
|
+
case value
|
46
|
+
when RecordPlaceholder
|
47
|
+
clean_record_placeholder( value, return_placeholders: return_placeholders )
|
48
|
+
when ActiveRecord::Base
|
49
|
+
clean_record( value, return_placeholders: return_placeholders )
|
50
|
+
when Array
|
51
|
+
clean_array( value, return_placeholders: return_placeholders )
|
52
|
+
else
|
53
|
+
value
|
54
|
+
end
|
55
|
+
|
56
|
+
ensure
|
57
|
+
__pop__
|
58
|
+
end
|
59
|
+
|
60
|
+
def clean_array( value, return_placeholders: )
|
61
|
+
placeholders = value.map {|e| clean(e, return_placeholders: true) }
|
62
|
+
if placeholders.all? { |e| e.kind_of?(RecordPlaceholder) } && !return_placeholders
|
63
|
+
count_placeholders( placeholders )
|
64
|
+
else
|
65
|
+
placeholders.map {|e| clean(e, return_placeholders: return_placeholders) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def clean_record( value, return_placeholders: )
|
70
|
+
placeholder = RecordPlaceholder.new( value )
|
71
|
+
clean_record_placeholder( placeholder, return_placeholders: return_placeholders )
|
72
|
+
end
|
73
|
+
|
74
|
+
def clean_record_placeholder( value, return_placeholders: )
|
75
|
+
return_placeholders \
|
76
|
+
? value \
|
77
|
+
: value.to_a
|
78
|
+
end
|
79
|
+
|
80
|
+
def count_placeholders( placeholders )
|
81
|
+
counts = placeholders.group_by(&:class_name).map { |class_name, summs|
|
82
|
+
[ class_name, summs.map(&:id) ]
|
83
|
+
}
|
84
|
+
Hash[ counts ]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|