attr-gather 1.2.0 → 1.2.1
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.
- checksums.yaml +4 -4
- data/.github/mergify.yml +4 -1
- data/.github/workflows/ruby.yml +3 -0
- data/.rubocop.yml +2 -1
- data/Gemfile.lock +26 -20
- data/README.md +142 -4
- data/attr-gather.gemspec +2 -0
- data/examples/post_enhancer.rb +1 -1
- data/lib/attr/gather/aggregators/deep_merge.rb +4 -7
- data/lib/attr/gather/aggregators/shallow_merge.rb +4 -2
- data/lib/attr/gather/filters/contract.rb +1 -1
- data/lib/attr/gather/version.rb +1 -1
- data/lib/attr/gather/workflow/dot_serializer.rb +1 -2
- data/lib/attr/gather/workflow/dsl.rb +55 -5
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56fa2344f15f389295db52f8201720d91bae7114d5d68472208d5509fe40d35d
|
4
|
+
data.tar.gz: cce04b8b3ebe30523f0b4b5528f2531d5607e1905e6869f21076ac7eeac39e34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dad243cf700400e4bab5abb9134704870885c711a6a4c4eedf185860af66485493f16d2a2fda99b103e1ab7b5238c244e16a73a7b1264b9f59cee6f42bbdf2b7
|
7
|
+
data.tar.gz: 46441ed69c96018c33c4676e3e48b3b06bf39de8aaa2f16c20fdda026375bd66330064c11ac8ea4f7499b0038c98309a3f958ea532ab6da1476dad2e5e070de1
|
data/.github/mergify.yml
CHANGED
@@ -13,7 +13,10 @@ pull_request_rules:
|
|
13
13
|
conditions:
|
14
14
|
- author~=^dependabot(|-preview)\[bot\]$
|
15
15
|
- "#approved-reviews-by>=1"
|
16
|
-
- "check-success
|
16
|
+
- "check-success~=build-test-lint (2.4.x)"
|
17
|
+
- "check-success~=build-test-lint (2.5.x)"
|
18
|
+
- "check-success~=build-test-lint (2.6.x)"
|
19
|
+
- "check-success~=build-test-lint (2.7.x)"
|
17
20
|
actions:
|
18
21
|
merge:
|
19
22
|
method: squash
|
data/.github/workflows/ruby.yml
CHANGED
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
attr-gather (1.2.
|
4
|
+
attr-gather (1.2.1)
|
5
5
|
dry-container (~> 0.7)
|
6
6
|
|
7
7
|
GEM
|
@@ -14,7 +14,7 @@ GEM
|
|
14
14
|
benchmark (0.1.0)
|
15
15
|
coderay (1.1.3)
|
16
16
|
concurrent-ruby (1.1.7)
|
17
|
-
diff-lcs (1.
|
17
|
+
diff-lcs (1.4.4)
|
18
18
|
domain_name (0.5.20190701)
|
19
19
|
unf (>= 0.0.5, < 1.0.0)
|
20
20
|
dry-configurable (0.11.6)
|
@@ -76,7 +76,7 @@ GEM
|
|
76
76
|
mini_portile2 (2.4.0)
|
77
77
|
nokogiri (1.10.10)
|
78
78
|
mini_portile2 (~> 2.4.0)
|
79
|
-
parallel (1.
|
79
|
+
parallel (1.20.1)
|
80
80
|
parser (2.7.2.0)
|
81
81
|
ast (~> 2.4.1)
|
82
82
|
pry (0.13.1)
|
@@ -85,29 +85,35 @@ GEM
|
|
85
85
|
public_suffix (4.0.6)
|
86
86
|
rainbow (3.0.0)
|
87
87
|
rake (13.0.1)
|
88
|
+
regexp_parser (2.0.0)
|
88
89
|
reverse_markdown (2.0.0)
|
89
90
|
nokogiri
|
90
|
-
|
91
|
-
|
92
|
-
rspec-
|
93
|
-
rspec-
|
94
|
-
|
95
|
-
|
96
|
-
|
91
|
+
rexml (3.2.4)
|
92
|
+
rspec (3.10.0)
|
93
|
+
rspec-core (~> 3.10.0)
|
94
|
+
rspec-expectations (~> 3.10.0)
|
95
|
+
rspec-mocks (~> 3.10.0)
|
96
|
+
rspec-core (3.10.0)
|
97
|
+
rspec-support (~> 3.10.0)
|
98
|
+
rspec-expectations (3.10.0)
|
97
99
|
diff-lcs (>= 1.2.0, < 2.0)
|
98
|
-
rspec-support (~> 3.
|
99
|
-
rspec-mocks (3.
|
100
|
+
rspec-support (~> 3.10.0)
|
101
|
+
rspec-mocks (3.10.0)
|
100
102
|
diff-lcs (>= 1.2.0, < 2.0)
|
101
|
-
rspec-support (~> 3.
|
102
|
-
rspec-support (3.
|
103
|
-
rubocop (0.
|
104
|
-
jaro_winkler (~> 1.5.1)
|
103
|
+
rspec-support (~> 3.10.0)
|
104
|
+
rspec-support (3.10.0)
|
105
|
+
rubocop (0.93.1)
|
105
106
|
parallel (~> 1.10)
|
106
|
-
parser (>= 2.
|
107
|
+
parser (>= 2.7.1.5)
|
107
108
|
rainbow (>= 2.2.2, < 4.0)
|
109
|
+
regexp_parser (>= 1.8)
|
110
|
+
rexml
|
111
|
+
rubocop-ast (>= 0.6.0)
|
108
112
|
ruby-progressbar (~> 1.7)
|
109
|
-
unicode-display_width (>= 1.4.0, <
|
110
|
-
rubocop-
|
113
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
114
|
+
rubocop-ast (1.3.0)
|
115
|
+
parser (>= 2.7.1.5)
|
116
|
+
rubocop-performance (1.6.1)
|
111
117
|
rubocop (>= 0.71.0)
|
112
118
|
ruby-progressbar (1.10.1)
|
113
119
|
solargraph (0.39.17)
|
@@ -129,7 +135,7 @@ GEM
|
|
129
135
|
unf (0.1.4)
|
130
136
|
unf_ext
|
131
137
|
unf_ext (0.0.7.7)
|
132
|
-
unicode-display_width (1.
|
138
|
+
unicode-display_width (1.7.0)
|
133
139
|
yard (0.9.25)
|
134
140
|
|
135
141
|
PLATFORMS
|
data/README.md
CHANGED
@@ -1,10 +1,148 @@
|
|
1
1
|
# attr-gather
|
2
2
|
|
3
|
-
|
3
|
+

|
4
|
+
[](https://codeclimate.com/github/ianks/attr-gather/maintainability)
|
4
5
|
|
5
|
-
A gem for creating
|
6
|
-
|
7
|
-
|
6
|
+
A gem for creating workflows that "enhance" entities with extra attributes. At a high level, [attr-gather](https://github.com/ianks/attr-gather) provides a process to fetch information from many data sources (such as third party APIs, legacy databases, etc.) in a fully parallelized fashion.
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
### Defining your workflow
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# define a workflow
|
14
|
+
class EnhanceProfile
|
15
|
+
include Attr::Gather::Workflow
|
16
|
+
|
17
|
+
# contains all the task implementations
|
18
|
+
container TasksContainer
|
19
|
+
|
20
|
+
# filter out invalid data using a Dry::Validation::Contract
|
21
|
+
# anything that doesn't match this schema will be filtered out
|
22
|
+
filter_with_contract do
|
23
|
+
params do
|
24
|
+
required(:user_id).filled(:integer)
|
25
|
+
|
26
|
+
optional(:user).hash do
|
27
|
+
optional(:name).filled(:string)
|
28
|
+
optional(:email).filled(:string)
|
29
|
+
optional(:gravatar).filled(:string)
|
30
|
+
optional(:email_info).hash do
|
31
|
+
optional(:deliverable).filled(:bool?)
|
32
|
+
optional(:free).filled(:bool?)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# each task returns a hash of data that will be merged into the result
|
39
|
+
task :fetch_post do |t|
|
40
|
+
t.depends_on = []
|
41
|
+
end
|
42
|
+
|
43
|
+
# will run in parallel
|
44
|
+
task :fetch_user do |t|
|
45
|
+
t.depends_on = [:fetch_post]
|
46
|
+
end
|
47
|
+
|
48
|
+
# will run in parallel
|
49
|
+
task :fetch_email_info do |t|
|
50
|
+
t.depends_on = [:fetch_user]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
### Defining some tasks
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class PostFetcher
|
59
|
+
def call(attrs)
|
60
|
+
res = HTTP.get("https://jsonplaceholder.typicode.com/posts/#{attrs[:id]}")
|
61
|
+
post = JSON.parse(res.to_s, symbolize_names: true)
|
62
|
+
|
63
|
+
{ title: post[:title], user_id: post[:userId], body: post[:body] }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class UserFetcher
|
70
|
+
# will have access to the PostFetcher attributes here
|
71
|
+
def call(attrs)
|
72
|
+
res = HTTP.get("https://jsonplaceholder.typicode.com/users/#{attrs[:user_id]}")
|
73
|
+
user = JSON.parse(res.to_s, symbolize_names: true)
|
74
|
+
|
75
|
+
{ user: { name: user[:name], email: user[:email] } }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class EmailInfoFetcher
|
82
|
+
# will have access to the PostFetcher attributes here
|
83
|
+
def call(user:)
|
84
|
+
res = HTTP.timeout(3).get("https://api.trumail.io/v2/lookups/json?email=#{user[:email]}")
|
85
|
+
info = JSON.parse(res.to_s, symbolize_names: true)
|
86
|
+
|
87
|
+
# will deep merge with the final result
|
88
|
+
{ user: { email_info: { deliverable: info[:deliverable], free: info[:free] } } }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
### Registering your tasks
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class MyContainer
|
97
|
+
extend Dry::Container::Mixin
|
98
|
+
|
99
|
+
register :fetch_post, PostFetcher
|
100
|
+
register :fetch_user, UserFetcher
|
101
|
+
register :fetch_email_info, EmailInfoFetcher
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### Run it!
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
enhancer = EnhanceUserProfile.new
|
109
|
+
enhancer.call(id: 12).value!
|
110
|
+
```
|
111
|
+
|
112
|
+
And this is the result...
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
{
|
116
|
+
:id => 12,
|
117
|
+
:user_id => 2,
|
118
|
+
:user => {
|
119
|
+
:email => "Shanna@melissa.tv",
|
120
|
+
:name => "Ervin Howell",
|
121
|
+
:email_info => { :deliverable => true, :free => true },
|
122
|
+
:gravatar => "https://www.gravatar.com/avatar/241af7d19a0a7438794aef21e4e19b79"
|
123
|
+
}
|
124
|
+
}
|
125
|
+
```
|
126
|
+
|
127
|
+
You can even preview it as an SVG!
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
enhancer.to_dot(preview: true) # requires graphviz (brew install graphviz)
|
131
|
+
```
|
132
|
+
|
133
|
+
## Features
|
134
|
+
|
135
|
+
- Offers DSL for defining workflows and merging the results from each task
|
136
|
+
- Execution engine optimally parallelizes the execution of the workflow dependency graph using [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) Promises
|
137
|
+
- Very easy to unit test
|
138
|
+
- Ability to filter out bad/junky data using [dry-validation](https://dry-rb.org/gems/dry-validation) contracts
|
139
|
+
|
140
|
+
## What are the main difference between this Ruby project and similar ones?
|
141
|
+
|
142
|
+
- Operates on a single entity rather than a list, so easily adoptable in existing systems
|
143
|
+
- Focuses on the "fetching" and filtering of data solely, and not transformation or storage
|
144
|
+
- Focuses on having a clean PORO interface to make testing simple
|
145
|
+
- Provides a declarative interface for merging results from many sources (APIs, legacy databases, etc.) which allows for prioritization
|
8
146
|
|
9
147
|
## Links
|
10
148
|
|
data/attr-gather.gemspec
CHANGED
@@ -15,6 +15,8 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.homepage = 'https://github.com/ianks/attr-gather'
|
16
16
|
spec.license = 'MIT'
|
17
17
|
|
18
|
+
spec.required_ruby_version = '>= 2.4'
|
19
|
+
|
18
20
|
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
19
21
|
|
20
22
|
spec.metadata['homepage_uri'] = spec.homepage
|
data/examples/post_enhancer.rb
CHANGED
@@ -28,7 +28,7 @@ class MyContainer
|
|
28
28
|
end
|
29
29
|
|
30
30
|
register :email_info do |user:, **_attrs|
|
31
|
-
res = HTTP.
|
31
|
+
res = HTTP.get("https://api.trumail.io/v2/lookups/json?email=#{user[:email]}")
|
32
32
|
info = JSON.parse(res.to_s, symbolize_names: true)
|
33
33
|
|
34
34
|
{ user: { email_info: { deliverable: info[:deliverable], free: info[:free] } } }
|
@@ -12,28 +12,25 @@ module Attr
|
|
12
12
|
# Initialize a new DeepMerge aggregator
|
13
13
|
#
|
14
14
|
# @param reverse [Boolean] deep merge results in reverse order
|
15
|
+
# @param merge_input [Boolean] merge the result with the initial input
|
15
16
|
#
|
16
17
|
# @api private
|
17
|
-
def initialize(reverse: false, **)
|
18
|
+
def initialize(reverse: false, merge_input: true, **)
|
18
19
|
@reverse = reverse
|
20
|
+
@merge_input = merge_input
|
19
21
|
super
|
20
22
|
end
|
21
23
|
|
22
24
|
def call(input, execution_results)
|
23
25
|
execution_results = execution_results.reverse_each if reverse?
|
24
|
-
initial = unwrap_initial_input(input)
|
25
26
|
|
26
|
-
execution_results.reduce(
|
27
|
+
execution_results.reduce(@merge_input ? input : EMPTY_HASH) do |memo, res|
|
27
28
|
deep_merge(memo, unwrap_result(res))
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
31
32
|
private
|
32
33
|
|
33
|
-
def unwrap_initial_input(input)
|
34
|
-
input
|
35
|
-
end
|
36
|
-
|
37
34
|
def reverse?
|
38
35
|
@reverse
|
39
36
|
end
|
@@ -12,17 +12,19 @@ module Attr
|
|
12
12
|
# Initialize a new DeepMerge aggregator
|
13
13
|
#
|
14
14
|
# @param reverse [Boolean] merge results in reverse order
|
15
|
+
# @param merge_input [Boolean] merge the result with the initial input
|
15
16
|
#
|
16
17
|
# @api private
|
17
|
-
def initialize(reverse: false, **)
|
18
|
+
def initialize(reverse: false, merge_input: true, **)
|
18
19
|
@reverse = reverse
|
20
|
+
@merge_input = merge_input
|
19
21
|
super
|
20
22
|
end
|
21
23
|
|
22
24
|
def call(input, execution_results)
|
23
25
|
execution_results = execution_results.reverse_each if reverse?
|
24
26
|
|
25
|
-
execution_results.reduce(input) do |memo, res|
|
27
|
+
execution_results.reduce(@merge_input ? input : EMPTY_HASH) do |memo, res|
|
26
28
|
memo.merge(unwrap_result(res))
|
27
29
|
end
|
28
30
|
end
|
data/lib/attr/gather/version.rb
CHANGED
@@ -33,8 +33,7 @@ module Attr
|
|
33
33
|
|
34
34
|
def serialize_row(task)
|
35
35
|
row = all_dependants_for_task(task).map { |dt| [task, dt] }
|
36
|
-
|
37
|
-
lines
|
36
|
+
row.map { |item| "#{item.map(&:name).join(' -> ')};" }
|
38
37
|
end
|
39
38
|
|
40
39
|
def all_dependants_for_task(input_task)
|
@@ -16,7 +16,7 @@ module Attr
|
|
16
16
|
#
|
17
17
|
# @example
|
18
18
|
# class EnhanceUserProfile
|
19
|
-
#
|
19
|
+
# include Attr::Gather::Workflow
|
20
20
|
#
|
21
21
|
# # ...
|
22
22
|
#
|
@@ -42,6 +42,56 @@ module Attr
|
|
42
42
|
self
|
43
43
|
end
|
44
44
|
|
45
|
+
# Defines a task with name and options
|
46
|
+
#
|
47
|
+
# @param task_name [Symbol] the name of the task
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# class EnhanceUserProfile
|
51
|
+
# include Attr::Gather::Workflow
|
52
|
+
#
|
53
|
+
# # ...
|
54
|
+
#
|
55
|
+
# fetch :user_info do |t|
|
56
|
+
# t.depends_on = [:fetch_gravatar_info]
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# Calling `fetch` will yield a task object which you can configure like
|
61
|
+
# a PORO. Tasks will be registered for execution in the workflow.
|
62
|
+
#
|
63
|
+
# @yield [Attr::Gather::Workflow::Task] A task to configure
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def fetch(task_name, opts = EMPTY_HASH)
|
67
|
+
task(task_name, opts)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Defines a task with name and options
|
71
|
+
#
|
72
|
+
# @param task_name [Symbol] the name of the task
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# class EnhanceUserProfile
|
76
|
+
# include Attr::Gather::Workflow
|
77
|
+
#
|
78
|
+
# # ...
|
79
|
+
#
|
80
|
+
# step :fetch_user_info do |t|
|
81
|
+
# t.depends_on = [:email_info]
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# Calling `step` will yield a task object which you can configure like
|
86
|
+
# a PORO. Tasks will be registered for execution in the workflow.
|
87
|
+
#
|
88
|
+
# @yield [Attr::Gather::Workflow::Task] A task to configure
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def step(task_name, opts = EMPTY_HASH)
|
92
|
+
task(task_name, opts)
|
93
|
+
end
|
94
|
+
|
45
95
|
# Defines a container for task dependencies
|
46
96
|
#
|
47
97
|
# Using a container makes it easy to re-use workflows with different
|
@@ -55,7 +105,7 @@ module Attr
|
|
55
105
|
# end
|
56
106
|
#
|
57
107
|
# class EnhanceUserProfile
|
58
|
-
#
|
108
|
+
# include Attr::Gather::Workflow
|
59
109
|
#
|
60
110
|
# container LegacySystem
|
61
111
|
# end
|
@@ -79,7 +129,7 @@ module Attr
|
|
79
129
|
#
|
80
130
|
# @example
|
81
131
|
# class EnhanceUserProfile
|
82
|
-
#
|
132
|
+
# include Attr::Gather::Workflow
|
83
133
|
#
|
84
134
|
# aggregator :deep_merge
|
85
135
|
# end
|
@@ -119,7 +169,7 @@ module Attr
|
|
119
169
|
# end
|
120
170
|
#
|
121
171
|
# class EnhanceUserProfile
|
122
|
-
#
|
172
|
+
# include Attr::Gather::Workflow
|
123
173
|
#
|
124
174
|
# # Any of the key/value pairs that had validation errors will be
|
125
175
|
# # filtered from the output.
|
@@ -151,7 +201,7 @@ module Attr
|
|
151
201
|
# @example
|
152
202
|
#
|
153
203
|
# class EnhanceUserProfile
|
154
|
-
#
|
204
|
+
# include Attr::Gather::Workflow
|
155
205
|
#
|
156
206
|
# # Any of the key/value pairs that had validation errors will be
|
157
207
|
# # filtered from the output.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attr-gather
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ian Ker-Seymer
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -135,7 +135,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version: '
|
138
|
+
version: '2.4'
|
139
139
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
140
|
requirements:
|
141
141
|
- - ">="
|