reforge 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b3954966aa5b1027a05381e015a54986920c1e08af4f92ea4cbbee550ec9547e
4
+ data.tar.gz: e9fd17106ca7608a4e1ee47a0b19e9d22e8fe300b098e5edac9b6f4a47451ae8
5
+ SHA512:
6
+ metadata.gz: 5c287390d5e5016dae3b0f30d0251c3b91ee8690b2f067674d550a156e8725b9c3c0b5b902794cda7d170fd6ce2e929c20dd2a273103b15af5dbd0d87637c119
7
+ data.tar.gz: 438364630d684abe90326e6aadd263687b504ccbd9b09f741849b5fd3462a0af6a77033902a1d4321b20c4b70c1196f85da3ff7ad61f1762365afad5cb3579eb
@@ -0,0 +1,76 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ env:
10
+ BUNDLE_PATH: vendor/bundle
11
+
12
+ jobs:
13
+ lint:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: 2.6.6
20
+ - name: Cache dependencies
21
+ id: cache-dependencies
22
+ uses: actions/cache@v2
23
+ with:
24
+ path: ${{ env.BUNDLE_PATH }}
25
+ key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
26
+ restore-keys: ${{ runner.os }}-gems-
27
+ - name: Install dependencies
28
+ if: steps.cache-dependencies.outputs.cache-hit != 'true'
29
+ run: bundle install --jobs 20 --retry 5
30
+ - name: Lint files
31
+ run: bundle exec rake rubocop
32
+
33
+ test:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v2
37
+ with:
38
+ fetch-depth: 2
39
+ - uses: ruby/setup-ruby@v1
40
+ with:
41
+ ruby-version: 2.6.6
42
+ - name: Cache dependencies
43
+ id: cache-dependencies
44
+ uses: actions/cache@v2
45
+ with:
46
+ path: ${{ env.BUNDLE_PATH }}
47
+ key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
48
+ restore-keys: ${{ runner.os }}-gems-
49
+ - name: Download test reporter
50
+ run: |
51
+ curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
52
+ chmod +x ./cc-test-reporter
53
+ - name: Notify of pending report
54
+ run: ./cc-test-reporter before-build
55
+ - name: Install dependencies
56
+ if: steps.cache-dependencies.outputs.cache-hit != 'true'
57
+ run: bundle install --jobs 20 --retry 5
58
+ - name: Run tests
59
+ run: bundle exec rake rspec
60
+ - name: Publish code coverage
61
+ # TRICKY: We need to manually set the env vars required by Code Climate. GIT_BRANCH is simple to determine, but
62
+ # GIT_COMMIT_SHA is context dependent:
63
+ # - When running actions on main when it is pushed we can simply use GITHUB_SHA
64
+ # - When running actions on a pull request actions/checkout@v2 creates a merge commit, so GITHUB_SHA is one
65
+ # commit ahead. We use the log to determine GIT_COMMIT_SHA by looking at the second parent of the merge commit,
66
+ # and set actions/checkout@v2's fetch-depth to 2 above to ensure that the commit is available
67
+ # (ref: https://docs.codeclimate.com/docs/github-actions-test-coverage)
68
+ run: |
69
+ export GIT_BRANCH="${GITHUB_HEAD_REF}"
70
+ if [ $GITHUB_EVENT_NAME == "pull_request" ]
71
+ then
72
+ export GIT_COMMIT_SHA="$(git log --pretty=%P -n 1 "${GITHUB_SHA}" | cut -d' ' -f2)"
73
+ else
74
+ export GIT_COMMIT_SHA="${GITHUB_SHA}"
75
+ fi
76
+ ./cc-test-reporter after-build -r ${{secrets.CC_TEST_REPORTER_ID}}
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ reforge-*.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ require: rubocop-performance
2
+
3
+ # overrides
4
+ Style/Documentation:
5
+ Enabled: false
6
+ Style/MutableConstant:
7
+ EnforcedStyle: strict
8
+ Style/StringLiterals:
9
+ EnforcedStyle: double_quotes
10
+
11
+ # opt-in new cops
12
+ AllCops:
13
+ NewCops: enable
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.6
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 2.6.6
data/CHANGELOG.md ADDED
File without changes
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at eizengan@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in reforge.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,89 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ reforge (0.1.0)
5
+ zeitwerk (~> 2.4)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.1)
11
+ attr_extras (6.2.4)
12
+ byebug (11.1.3)
13
+ coderay (1.1.3)
14
+ diff-lcs (1.3)
15
+ docile (1.3.2)
16
+ json (2.3.1)
17
+ method_source (1.0.0)
18
+ parallel (1.19.2)
19
+ parser (2.7.2.0)
20
+ ast (~> 2.4.1)
21
+ patience_diff (1.1.0)
22
+ trollop (~> 1.16)
23
+ pry (0.13.1)
24
+ coderay (~> 1.1)
25
+ method_source (~> 1.0)
26
+ pry-byebug (3.9.0)
27
+ byebug (~> 11.0)
28
+ pry (~> 0.13.0)
29
+ rainbow (3.0.0)
30
+ rake (12.3.3)
31
+ regexp_parser (1.8.2)
32
+ rexml (3.2.4)
33
+ rspec (3.9.0)
34
+ rspec-core (~> 3.9.0)
35
+ rspec-expectations (~> 3.9.0)
36
+ rspec-mocks (~> 3.9.0)
37
+ rspec-core (3.9.2)
38
+ rspec-support (~> 3.9.3)
39
+ rspec-expectations (3.9.2)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.9.0)
42
+ rspec-mocks (3.9.1)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.9.0)
45
+ rspec-support (3.9.3)
46
+ rubocop (1.1.0)
47
+ parallel (~> 1.10)
48
+ parser (>= 2.7.1.5)
49
+ rainbow (>= 2.2.2, < 4.0)
50
+ regexp_parser (>= 1.8)
51
+ rexml
52
+ rubocop-ast (>= 1.0.1)
53
+ ruby-progressbar (~> 1.7)
54
+ unicode-display_width (>= 1.4.0, < 2.0)
55
+ rubocop-ast (1.1.0)
56
+ parser (>= 2.7.1.5)
57
+ rubocop-performance (1.8.1)
58
+ rubocop (>= 0.87.0)
59
+ rubocop-ast (>= 0.4.0)
60
+ ruby-progressbar (1.10.1)
61
+ simplecov (0.17.1)
62
+ docile (~> 1.1)
63
+ json (>= 1.8, < 3)
64
+ simplecov-html (~> 0.10.0)
65
+ simplecov-html (0.10.2)
66
+ super_diff (0.5.2)
67
+ attr_extras (>= 6.2.4)
68
+ diff-lcs
69
+ patience_diff
70
+ trollop (1.16.2)
71
+ unicode-display_width (1.7.0)
72
+ zeitwerk (2.4.0)
73
+
74
+ PLATFORMS
75
+ ruby
76
+
77
+ DEPENDENCIES
78
+ bundler (~> 2.0)
79
+ pry-byebug (~> 3.9)
80
+ rake (~> 12.0)
81
+ reforge!
82
+ rspec (~> 3.0)
83
+ rubocop (~> 1.0)
84
+ rubocop-performance (~> 1.8)
85
+ simplecov (~> 0.17.1)
86
+ super_diff (~> 0.5)
87
+
88
+ BUNDLED WITH
89
+ 2.1.4
data/INTRODUCTION.md ADDED
@@ -0,0 +1,274 @@
1
+ # Introduction
2
+
3
+ This guide outlines the basic knowledge required to use Reforge in a project. Navigation links for the guide are provided below for your convenience.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Getting Started](#getting-started)
9
+ - [Terminology](#terminology)
10
+ - [The Transformation DSL](#the-transformation-dsl)
11
+ - [Defining Paths](#defining-paths)
12
+ - [Defining Transforms](#defining-transforms)
13
+ - [Attribute Configuration Hashes](#attribute-configuration-hashes)
14
+ - [Key Configuration Hashes](#key-configuration-hashes)
15
+ - [Nil Propogation](#nil-propogation)
16
+ - [Value Configuration Hashes](#value-configuration-hashes)
17
+ - [Memoizing Transform Results](#memoizing-transform-results)
18
+ - [Memo Lifetime](#memo-lifetime)
19
+ - [Simple Memoization](#simple-memoization)
20
+ - [Calculating A Transform Only Once](#calculating-a-transform-only-once)
21
+ - [Custom Memoization](#custom-memoization)
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem 'reforge'
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ $ bundle
34
+
35
+ Or install it yourself as:
36
+
37
+ $ gem install reforge
38
+
39
+ ## Terminology
40
+
41
+ **Transformation** : The most crucial class provided by Reforge. Provides a simple DSL to define one or more *Pathed Transforms*, and uses them to convert a *Source* into a *Result*.
42
+
43
+ **Pathed Transform** : A pairing of a *Transform* and a *Path*. Describes both how to obtain a value from a *Source* and a location to place it within the *Result*.
44
+
45
+ **Transform** : A description of how to obtain a single, transformed value from a *Source*.
46
+
47
+ **Path** : A description of a location within a *Result*. Typically a Symbol/String key when the *Result* is a Hash, or an Integer index when the *Result* is an Array.
48
+
49
+ **Source** : An object which a *Transformation* can convert into a *Result*. Can be any object, but all the *Transformation's* constituent *Transforms* must succeed when processing it.
50
+
51
+ **Result** : The object created by a *Transformation* from a *Source* by aggregating the output of all its *Pathed Transforms*.
52
+
53
+ ## Getting Started
54
+
55
+ The following example illustrates how to create a Transformation and transform your data:
56
+
57
+ ```ruby
58
+ require "reforge"
59
+ require "date"
60
+
61
+ def expensive_method
62
+ puts "expensive_method was called, but only once!"
63
+ sleep(10)
64
+ "hello world"
65
+ end
66
+
67
+ class ExampleTransformation < Reforge::Transformation
68
+ extract :date, from: ->(source) { Date.parse(source[:timestamp]) }
69
+ extract :amount, from: { key: :usd }
70
+ extract :expensive_result, from: -> { expensive_method }, memoize: :first
71
+ end
72
+
73
+ sources = [
74
+ { timestamp: "2019-01-01", usd: 1.23 },
75
+ { timestamp: "2019-01-10", usd: 2.46 },
76
+ { timestamp: "2019-10-01", usd: 3.69 }
77
+ ]
78
+ results = ExampleTransformation.call(*sources)
79
+ # expensive_method was called, but only once!
80
+ # => [{:date=>#<Date: 2019-01-01 ...>, :amount=>1.23, :expensive_result=>"hello world"},
81
+ # {:date=>#<Date: 2019-01-10 ...>, :amount=>2.46, :expensive_result=>"hello world"},
82
+ # {:date=>#<Date: 2019-10-01 ...>, :amount=>3.69, :expensive_result=>"hello world"}]
83
+ ```
84
+
85
+ You can glean the following from the above example:
86
+
87
+ - Transformations are created by inheriting the `Reforge::Transformation` class
88
+ - The Result of a Transformation is described in parts by using the `.extract` DSL method
89
+ - The `.extract` DSL method takes a Path to determine where in the Result to place transformed Source data
90
+ - The `.extract` DSL method takes a `from:` keyword argument to determine a Transform to perform on the Source data
91
+ - Transforms of Source data can be defined by a proc or derived from a special configuration hash
92
+ - The `.extract` DSL method takes an optional `memoize:` keyword argument which can sometimes be used to avoid repeating expensive Transforms
93
+
94
+ These are all described in greater detail below, and are linked in the [Table of Contents](#table-of-contents).
95
+
96
+ ## The Transformation DSL
97
+
98
+ Inheriting from the `Reforge::Transformation` class grants access to the DSL methods outlined in the example below:
99
+
100
+ ```ruby
101
+ class DSLExamples < Reforge::Transformation
102
+ extract path_definition, from: transform_definition, memoize: memoize_options
103
+ transform transform_definition, into: path_definition, memoize: memoize_options
104
+ end
105
+ ```
106
+
107
+ The `.extract` and `.transform` methods are used to define Pathed Transforms, and cause identical results when given the same arguments (as above). You may use whichever method better suits your use case, although this documentation prefers `.extract`. The path_definition must [define a valid Path](#defining-paths). The transform_definition must [define a valid Transform](#defining-transforms). The memoize parameter is optional, and when included is used to [avoid repeating expensive Transforms](#memoizing-transform-results).
108
+
109
+ Errors resulting from improper usage of the DSL are not raised immediately. Instead they are deferred until the Transformation containing the improper DSL calls is instantiated or otherwise used. This ensures an application using Reforge will not break outside contexts where improper DSL usage could adversely affect its behavior.
110
+
111
+ ## Defining Paths
112
+
113
+ Paths are used by Reforge to determine where in the Result to place transformed Source data. They are also used to determine what type the Result will take - a path of `:key` or `"key"` will result in a Hash, and `0` in an Array. A Path with Array type can be used when values should be nested within a series of Hashes and Arrays; the regular type deduction rules will be applied in order. The following examples illustrate what happens when the string `"data"` is placed using the path definitions shown:
114
+
115
+ ```ruby
116
+ path = :foo # result = { foo: "data" }
117
+ path = "foo" # result = { "foo" => "data" }
118
+ path = 0 # result = ["data"]
119
+ path = [:foo, 0] # result = { foo: ["data"] }
120
+ path = %i[foo bar] # result = { foo: { bar: "data" } }
121
+ ```
122
+
123
+ ## Defining Transforms
124
+
125
+ Transforms are used by Reforge to determine how to obtain a single, transformed value from a Source. A Transform can be a Proc which takes the Source as an argument or a Proc which takes no arguments. For some simple Transforms a Configuration Hash can be used instead.
126
+
127
+ ### Transform Configuration Hashes
128
+
129
+ #### Key Configuration Hashes
130
+
131
+ If the data you need from a Source exists inside nested Hash- or Array-like objects, then you can use a Transform defined by a key configuration hash to obtain it. The pairs of Transforms below will produce the same results:
132
+ ```ruby
133
+ transform = ->(source) { source[:foo] }
134
+ transform = { key: :foo }
135
+
136
+ nested_transform = ->(source) { source[:foo][1] }
137
+ nested_transform = { key: [:foo, 1] }
138
+ ```
139
+
140
+ #### Attribute Configuration Hashes
141
+
142
+ Similarly if the data you need lies at the end of a series of attribute calls, then you can use an attribute configuration hash to obtain it. The pairs of Transforms below will produce the same results:
143
+ ```ruby
144
+ transform = ->(source) { source.size }
145
+ transform = { attribute: :size }
146
+
147
+ nested_transform = ->(source) { source.size.digits }
148
+ nested_transform = { attribute: %i[size digits] }
149
+
150
+ ```
151
+
152
+ #### Nil Propogation
153
+
154
+ The Transforms made by key and attribute configuration hashes will raise an error if they hit a nil value partway through the array of keys or attributes they have been instructed to traverse. To avoid this, you may pass the option `propogate_nil: true`. These Transforms will produce the same results:
155
+ ```ruby
156
+ transform = ->(source) { source&.size&.digits }
157
+ transform = { attribute: %i[size digits], propogate_nil: true }
158
+ ```
159
+
160
+ #### Value Configuration Hashes
161
+
162
+ Sometimes the data you need to add to a Result is independent of the Source and will not change. In such cases you can use a value configuration hash. These Transforms will produce the same results:
163
+ ```ruby
164
+ transform = -> { 1_234_567 }
165
+ transform = { value: 1_234_567 }
166
+ ```
167
+
168
+ ## Memoizing Transform Results
169
+
170
+ There may be occasions where your Transforms are expensive either in the time required to precess them or their resource use. The initial cost of these Transforms may be unavoidable, but by memoizing the Transform results you can avoid incurring it when the Transform is repeated. By specifying what criteria qualify as "repitition", you can tailor memoization perfectly to your use case.
171
+
172
+ ### Memo Lifetime
173
+
174
+ Memoized results will last as long as the Transformation that uses them. For calls directly against the class this duration is equal the duration of the call, and for calls against a Transformation instance it is equal the lifetime of that instance:
175
+
176
+ ```ruby
177
+ class ExampleTransformation < Reforge::Transformation
178
+ extract :expensive_result, from: -> { expensive_method }, memoize: :true
179
+ end
180
+
181
+ # The memo used in this call lasts only for the duration of that call - future calls are unaffected
182
+ ExampleTransformation.call(*sources)
183
+
184
+
185
+ # A Transformation instance keeps its memo between calls...
186
+ transformation = ExampleTransformation.new
187
+ transform.call(*sources)
188
+ # ...so since all the sources have already been memoized by the call above, the call below only ever uses the
189
+ # memoized results
190
+ transform.call(*sources)
191
+ ```
192
+
193
+ ### Simple Memoization
194
+
195
+ Basic memoization is performed by providing the `memoize: true` option to the DSL's Transform creation methods. This style of memoization stores the Transform result by its source, and future calls will use this result instead of recalculating it:
196
+
197
+ ```ruby
198
+ def expensive_method(i)
199
+ puts "expensive_method was called!"
200
+ sleep(10)
201
+ "result for #{i}"
202
+ end
203
+
204
+ class ExampleTransformation < Reforge::Transformation
205
+ extract :expensive_result, from: ->(i) { expensive_method(i) }, memoize: :true
206
+ end
207
+
208
+ sources = [1, 1]
209
+ results = ExampleTransformation.call(*sources)
210
+ # expensive_method was called!
211
+ # => [{:expensive_result=>"result for 1"},
212
+ # {:expensive_result=>"result for 1"}]
213
+
214
+ sources = [1, 2, 2]
215
+ results = ExampleTransformation.call(*sources)
216
+ # expensive_method was called!
217
+ # expensive_method was called!
218
+ # => [{:expensive_result=>"result for 1"},
219
+ # {:expensive_result=>"result for 2"},
220
+ # {:expensive_result=>"result for 2"}]
221
+ ```
222
+
223
+ ### Calculating A Transform Only Once
224
+
225
+ You may find cases where you only need to calculate the results of a Transform once. As an example, you may want to add a timestamp depicting when the Transformation was called to the Result. This can be done by supplying the DSL's Transform creation methods with the `memoize: :first` option. This stores the first value for the first Transform call, and will continue using it for all future calls regardless of the Source:
226
+
227
+ ```ruby
228
+ def current_time
229
+ puts "calculating current time"
230
+ Time.now
231
+ end
232
+
233
+ class ExampleTransformation < Reforge::Transformation
234
+ extract :timestamp, from: -> { current_time }, memoize: :first
235
+ end
236
+
237
+ sources = [*1..5]
238
+ results = ExampleTransformation.call(*sources)
239
+ # calculating current time
240
+ # => [{:timestamp=>2020-12-10 18:29:52 -0500},
241
+ # {:timestamp=>2020-12-10 18:29:52 -0500},
242
+ # {:timestamp=>2020-12-10 18:29:52 -0500},
243
+ # {:timestamp=>2020-12-10 18:29:52 -0500},
244
+ # {:timestamp=>2020-12-10 18:29:52 -0500}]
245
+ ```
246
+
247
+ ### Custom Memoization
248
+
249
+ You may create your own memoization schemes in cases where memoization is required but none of the built-in schemes will work. As an example, perhaps an attribute of the source contains a substring which corresponds to an ID in your database; in other words: the transform result should be memoized by that substring, not by the source itself. This can be accomplished by providing a configuration hash to the memoize option, such as `memoize: { by: ->(s) { s.id_attr[0..10] } }`. This configuration hash must contain the `:by` key, which must contain a [valid transform definition](#defining-transforms) - either a Proc or a configuration hash. When the transform specified at the `:by` key results in a value which was already calculated, then the memoized transform will return the stored result:
250
+
251
+ ```ruby
252
+ def find_customer(customer_id)
253
+ puts "finding customer with id='#{id}'"
254
+ Customer.find(customer_id)
255
+ end
256
+
257
+ class ExampleTransformation < Reforge::Transformation
258
+ extract :customer,
259
+ from: ->(source) { find_customer(source[:purchase_order].split("-").first) },
260
+ memoize: { by: ->(source) { source[:purchase_order].split("-").first } }
261
+ end
262
+
263
+ sources = [
264
+ { purchase_order: "123-2020-12-01" },
265
+ { purchase_order: "123-2020-12-02" },
266
+ { purchase_order: "456-2020-12-01" }
267
+ ]
268
+ results = ExampleTransformation.call(*sources)
269
+ # finding customer with id='123'
270
+ # finding customer with id='456'
271
+ # => [{:customer=>#<Retailer id: 123, ...>,
272
+ # {:customer=>#<Retailer id: 123, ...>,
273
+ # {:customer=>#<Retailer id: 456, ...>]
274
+ ```