reforge 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/.github/workflows/ci.yml +76 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.rubocop.yml +13 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +89 -0
- data/INTRODUCTION.md +274 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/reforge.rb +17 -0
- data/lib/reforge/configuration.rb +6 -0
- data/lib/reforge/transformation.rb +23 -0
- data/lib/reforge/transformation/dsl.rb +51 -0
- data/lib/reforge/transformation/transform.rb +43 -0
- data/lib/reforge/transformation/transform/factories.rb +61 -0
- data/lib/reforge/transformation/transform/memo.rb +46 -0
- data/lib/reforge/transformation/tree.rb +88 -0
- data/lib/reforge/transformation/tree/aggregate_node.rb +65 -0
- data/lib/reforge/transformation/tree/aggregate_node/array_node.rb +27 -0
- data/lib/reforge/transformation/tree/aggregate_node/factories.rb +32 -0
- data/lib/reforge/transformation/tree/aggregate_node/hash_node.rb +25 -0
- data/lib/reforge/transformation/tree/transform_node.rb +33 -0
- data/lib/reforge/zeitwerk.rb +16 -0
- data/reforge.gemspec +47 -0
- metadata +203 -0
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
data/.rspec
ADDED
data/.rubocop.yml
ADDED
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
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
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
|
+
```
|