attr-gather 1.1.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/mergify.yml +4 -1
- data/.github/workflows/deploy.yml +1 -1
- data/.github/workflows/ruby.yml +3 -0
- data/.rubocop.yml +2 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +68 -57
- data/README.md +141 -4
- data/attr-gather.gemspec +2 -0
- data/examples/post_enhancer.rb +1 -1
- data/lib/attr/gather/aggregators.rb +4 -4
- data/lib/attr/gather/aggregators/base.rb +11 -9
- data/lib/attr/gather/aggregators/deep_merge.rb +6 -3
- data/lib/attr/gather/aggregators/shallow_merge.rb +2 -4
- data/lib/attr/gather/concerns/registrable.rb +2 -2
- data/lib/attr/gather/filters/contract.rb +1 -1
- data/lib/attr/gather/version.rb +1 -1
- data/lib/attr/gather/workflow/callable.rb +16 -22
- data/lib/attr/gather/workflow/dot_serializer.rb +1 -2
- data/lib/attr/gather/workflow/dsl.rb +76 -22
- data/lib/attr/gather/workflow/task.rb +15 -2
- data/lib/attr/gather/workflow/task_graph.rb +14 -2
- metadata +6 -9
- data/lib/attr/gather/workflow/async_task_executor.rb +0 -17
- data/lib/attr/gather/workflow/task_execution_result.rb +0 -58
- data/lib/attr/gather/workflow/task_executor.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: edfd2d7479248348551d31676b45459f6f8279f6ca5fdf6ed00540c791de86bd
|
4
|
+
data.tar.gz: 977ac39f355ded8101507de998a221354ba357bad66528f9efbbb50200d4d731
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 958601e4741c30c6707ee6808fb89a258dfa10bab373f623d63fed29faef5eaf0d54d8fecfba4e9949bd16afb7d23bd84aca80aa2d485243de84076e2087284a
|
7
|
+
data.tar.gz: 11d1ae7d926743a202413647e1fe0f75e3c47966c9444c83a25e706ab8b923fa37ea2cda565042634d0cf6abc900be5fa2e1d3b47c436e6efc950a5d6f3d8fde
|
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
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
attr-gather (1.
|
4
|
+
attr-gather (1.3.0)
|
5
5
|
dry-container (~> 0.7)
|
6
6
|
|
7
7
|
GEM
|
@@ -9,123 +9,134 @@ GEM
|
|
9
9
|
specs:
|
10
10
|
addressable (2.7.0)
|
11
11
|
public_suffix (>= 2.0.2, < 5.0)
|
12
|
-
ast (2.4.
|
12
|
+
ast (2.4.1)
|
13
13
|
backport (1.1.2)
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
benchmark (0.1.0)
|
15
|
+
coderay (1.1.3)
|
16
|
+
concurrent-ruby (1.1.7)
|
17
|
+
diff-lcs (1.4.4)
|
17
18
|
domain_name (0.5.20190701)
|
18
19
|
unf (>= 0.0.5, < 1.0.0)
|
19
|
-
dry-configurable (0.
|
20
|
+
dry-configurable (0.11.6)
|
20
21
|
concurrent-ruby (~> 1.0)
|
21
22
|
dry-core (~> 0.4, >= 0.4.7)
|
23
|
+
dry-equalizer (~> 0.2)
|
22
24
|
dry-container (0.7.2)
|
23
25
|
concurrent-ruby (~> 1.0)
|
24
26
|
dry-configurable (~> 0.1, >= 0.1.3)
|
25
27
|
dry-core (0.4.9)
|
26
28
|
concurrent-ruby (~> 1.0)
|
27
|
-
dry-equalizer (0.
|
29
|
+
dry-equalizer (0.3.0)
|
28
30
|
dry-inflector (0.2.0)
|
29
|
-
dry-initializer (3.0.
|
30
|
-
dry-logic (1.0.
|
31
|
+
dry-initializer (3.0.4)
|
32
|
+
dry-logic (1.0.8)
|
31
33
|
concurrent-ruby (~> 1.0)
|
32
34
|
dry-core (~> 0.2)
|
33
35
|
dry-equalizer (~> 0.2)
|
34
|
-
dry-schema (1.
|
36
|
+
dry-schema (1.5.5)
|
35
37
|
concurrent-ruby (~> 1.0)
|
36
38
|
dry-configurable (~> 0.8, >= 0.8.3)
|
37
39
|
dry-core (~> 0.4)
|
38
40
|
dry-equalizer (~> 0.2)
|
39
41
|
dry-initializer (~> 3.0)
|
40
42
|
dry-logic (~> 1.0)
|
41
|
-
dry-types (~> 1.
|
42
|
-
dry-types (1.
|
43
|
+
dry-types (~> 1.4)
|
44
|
+
dry-types (1.4.0)
|
43
45
|
concurrent-ruby (~> 1.0)
|
44
46
|
dry-container (~> 0.3)
|
45
47
|
dry-core (~> 0.4, >= 0.4.4)
|
46
|
-
dry-equalizer (~> 0.
|
48
|
+
dry-equalizer (~> 0.3)
|
47
49
|
dry-inflector (~> 0.1, >= 0.1.2)
|
48
50
|
dry-logic (~> 1.0, >= 1.0.2)
|
49
|
-
dry-validation (1.
|
51
|
+
dry-validation (1.5.6)
|
50
52
|
concurrent-ruby (~> 1.0)
|
51
53
|
dry-container (~> 0.7, >= 0.7.1)
|
52
54
|
dry-core (~> 0.4)
|
53
55
|
dry-equalizer (~> 0.2)
|
54
56
|
dry-initializer (~> 3.0)
|
55
|
-
dry-schema (~> 1.
|
56
|
-
|
57
|
+
dry-schema (~> 1.5, >= 1.5.2)
|
58
|
+
e2mmap (0.1.0)
|
59
|
+
ffi (1.13.1)
|
57
60
|
ffi-compiler (1.0.1)
|
58
61
|
ffi (>= 1.0.0)
|
59
62
|
rake
|
60
|
-
|
61
|
-
http (4.2.0)
|
63
|
+
http (4.4.1)
|
62
64
|
addressable (~> 2.3)
|
63
65
|
http-cookie (~> 1.0)
|
64
|
-
http-form_data (~> 2.
|
66
|
+
http-form_data (~> 2.2)
|
65
67
|
http-parser (~> 1.2.0)
|
66
68
|
http-cookie (1.0.3)
|
67
69
|
domain_name (~> 0.5)
|
68
|
-
http-form_data (2.
|
70
|
+
http-form_data (2.3.0)
|
69
71
|
http-parser (1.2.1)
|
70
72
|
ffi-compiler (>= 1.0, < 2.0)
|
71
|
-
jaro_winkler (1.5.
|
72
|
-
|
73
|
+
jaro_winkler (1.5.4)
|
74
|
+
maruku (0.7.3)
|
75
|
+
method_source (1.0.0)
|
73
76
|
mini_portile2 (2.4.0)
|
74
|
-
nokogiri (1.10.
|
77
|
+
nokogiri (1.10.10)
|
75
78
|
mini_portile2 (~> 2.4.0)
|
76
|
-
parallel (1.
|
77
|
-
parser (2.
|
78
|
-
ast (~> 2.4.
|
79
|
-
pry (0.
|
80
|
-
coderay (~> 1.1
|
81
|
-
method_source (~>
|
82
|
-
public_suffix (4.0.
|
79
|
+
parallel (1.20.1)
|
80
|
+
parser (2.7.2.0)
|
81
|
+
ast (~> 2.4.1)
|
82
|
+
pry (0.13.1)
|
83
|
+
coderay (~> 1.1)
|
84
|
+
method_source (~> 1.0)
|
85
|
+
public_suffix (4.0.6)
|
83
86
|
rainbow (3.0.0)
|
84
87
|
rake (13.0.1)
|
85
|
-
|
88
|
+
regexp_parser (2.0.0)
|
89
|
+
reverse_markdown (2.0.0)
|
86
90
|
nokogiri
|
87
|
-
|
88
|
-
|
89
|
-
rspec-
|
90
|
-
rspec-
|
91
|
-
|
92
|
-
|
93
|
-
|
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)
|
94
99
|
diff-lcs (>= 1.2.0, < 2.0)
|
95
|
-
rspec-support (~> 3.
|
96
|
-
rspec-mocks (3.
|
100
|
+
rspec-support (~> 3.10.0)
|
101
|
+
rspec-mocks (3.10.0)
|
97
102
|
diff-lcs (>= 1.2.0, < 2.0)
|
98
|
-
rspec-support (~> 3.
|
99
|
-
rspec-support (3.
|
100
|
-
rubocop (0.
|
101
|
-
jaro_winkler (~> 1.5.1)
|
103
|
+
rspec-support (~> 3.10.0)
|
104
|
+
rspec-support (3.10.0)
|
105
|
+
rubocop (0.93.1)
|
102
106
|
parallel (~> 1.10)
|
103
|
-
parser (>= 2.
|
107
|
+
parser (>= 2.7.1.5)
|
104
108
|
rainbow (>= 2.2.2, < 4.0)
|
109
|
+
regexp_parser (>= 1.8)
|
110
|
+
rexml
|
111
|
+
rubocop-ast (>= 0.6.0)
|
105
112
|
ruby-progressbar (~> 1.7)
|
106
|
-
unicode-display_width (>= 1.4.0, <
|
107
|
-
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)
|
108
117
|
rubocop (>= 0.71.0)
|
109
118
|
ruby-progressbar (1.10.1)
|
110
|
-
solargraph (0.
|
119
|
+
solargraph (0.39.17)
|
111
120
|
backport (~> 1.1)
|
121
|
+
benchmark
|
112
122
|
bundler (>= 1.17.2)
|
113
|
-
|
123
|
+
e2mmap
|
114
124
|
jaro_winkler (~> 1.5)
|
125
|
+
maruku (~> 0.7, >= 0.7.3)
|
115
126
|
nokogiri (~> 1.9, >= 1.9.1)
|
116
127
|
parser (~> 2.3)
|
117
|
-
reverse_markdown (
|
128
|
+
reverse_markdown (>= 1.0.5, < 3)
|
118
129
|
rubocop (~> 0.52)
|
119
|
-
thor (~>
|
130
|
+
thor (~> 1.0)
|
120
131
|
tilt (~> 2.0)
|
121
|
-
yard (~> 0.9)
|
122
|
-
thor (0.
|
132
|
+
yard (~> 0.9, >= 0.9.24)
|
133
|
+
thor (1.0.1)
|
123
134
|
tilt (2.0.10)
|
124
135
|
unf (0.1.4)
|
125
136
|
unf_ext
|
126
|
-
unf_ext (0.0.7.
|
127
|
-
unicode-display_width (1.
|
128
|
-
yard (0.9.
|
137
|
+
unf_ext (0.0.7.7)
|
138
|
+
unicode-display_width (1.7.0)
|
139
|
+
yard (0.9.25)
|
129
140
|
|
130
141
|
PLATFORMS
|
131
142
|
ruby
|
@@ -133,7 +144,7 @@ PLATFORMS
|
|
133
144
|
DEPENDENCIES
|
134
145
|
attr-gather!
|
135
146
|
bundler (~> 2.0)
|
136
|
-
dry-validation (~> 1.
|
147
|
+
dry-validation (~> 1.5)
|
137
148
|
http
|
138
149
|
pry
|
139
150
|
rake (~> 13.0)
|
data/README.md
CHANGED
@@ -1,10 +1,147 @@
|
|
1
1
|
# attr-gather
|
2
2
|
|
3
|
-
|
3
|
+
![Actions Status](https://github.com/ianks/attr-gather/workflows/Build%20+%20Test%20+%20Lint/badge.svg)
|
4
4
|
|
5
|
-
A gem for creating
|
6
|
-
|
7
|
-
|
5
|
+
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.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
### Defining your workflow
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# define a workflow
|
13
|
+
class EnhanceProfile
|
14
|
+
include Attr::Gather::Workflow
|
15
|
+
|
16
|
+
# contains all the task implementations
|
17
|
+
container TasksContainer
|
18
|
+
|
19
|
+
# filter out invalid data using a Dry::Validation::Contract
|
20
|
+
# anything that doesn't match this schema will be filtered out
|
21
|
+
filter_with_contract do
|
22
|
+
params do
|
23
|
+
required(:user_id).filled(:integer)
|
24
|
+
|
25
|
+
optional(:user).hash do
|
26
|
+
optional(:name).filled(:string)
|
27
|
+
optional(:email).filled(:string)
|
28
|
+
optional(:gravatar).filled(:string)
|
29
|
+
optional(:email_info).hash do
|
30
|
+
optional(:deliverable).filled(:bool?)
|
31
|
+
optional(:free).filled(:bool?)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# each task returns a hash of data that will be merged into the result
|
38
|
+
task :fetch_post do |t|
|
39
|
+
t.depends_on = []
|
40
|
+
end
|
41
|
+
|
42
|
+
# will run in parallel
|
43
|
+
task :fetch_user do |t|
|
44
|
+
t.depends_on = [:fetch_post]
|
45
|
+
end
|
46
|
+
|
47
|
+
# will run in parallel
|
48
|
+
task :fetch_email_info do |t|
|
49
|
+
t.depends_on = [:fetch_user]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
### Defining some tasks
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class PostFetcher
|
58
|
+
def call(attrs)
|
59
|
+
res = HTTP.get("https://jsonplaceholder.typicode.com/posts/#{attrs[:id]}")
|
60
|
+
post = JSON.parse(res.to_s, symbolize_names: true)
|
61
|
+
|
62
|
+
{ title: post[:title], user_id: post[:userId], body: post[:body] }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class UserFetcher
|
69
|
+
# will have access to the PostFetcher attributes here
|
70
|
+
def call(attrs)
|
71
|
+
res = HTTP.get("https://jsonplaceholder.typicode.com/users/#{attrs[:user_id]}")
|
72
|
+
user = JSON.parse(res.to_s, symbolize_names: true)
|
73
|
+
|
74
|
+
{ user: { name: user[:name], email: user[:email] } }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class EmailInfoFetcher
|
81
|
+
# will have access to the PostFetcher attributes here
|
82
|
+
def call(user:)
|
83
|
+
res = HTTP.timeout(3).get("https://api.trumail.io/v2/lookups/json?email=#{user[:email]}")
|
84
|
+
info = JSON.parse(res.to_s, symbolize_names: true)
|
85
|
+
|
86
|
+
# will deep merge with the final result
|
87
|
+
{ user: { email_info: { deliverable: info[:deliverable], free: info[:free] } } }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
### Registering your tasks
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class MyContainer
|
96
|
+
extend Dry::Container::Mixin
|
97
|
+
|
98
|
+
register :fetch_post, PostFetcher
|
99
|
+
register :fetch_user, UserFetcher
|
100
|
+
register :fetch_email_info, EmailInfoFetcher
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### Run it!
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
enhancer = EnhanceUserProfile.new
|
108
|
+
enhancer.call(id: 12).value!
|
109
|
+
```
|
110
|
+
|
111
|
+
And this is the result...
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
{
|
115
|
+
:id => 12,
|
116
|
+
:user_id => 2,
|
117
|
+
:user => {
|
118
|
+
:email => "Shanna@melissa.tv",
|
119
|
+
:name => "Ervin Howell",
|
120
|
+
:email_info => { :deliverable => true, :free => true },
|
121
|
+
:gravatar => "https://www.gravatar.com/avatar/241af7d19a0a7438794aef21e4e19b79"
|
122
|
+
}
|
123
|
+
}
|
124
|
+
```
|
125
|
+
|
126
|
+
You can even preview it as an SVG!
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
enhancer.to_dot(preview: true) # requires graphviz (brew install graphviz)
|
130
|
+
```
|
131
|
+
|
132
|
+
## Features
|
133
|
+
|
134
|
+
- Offers DSL for defining workflows and merging the results from each task
|
135
|
+
- Execution engine optimally parallelizes the execution of the workflow dependency graph using [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) Promises
|
136
|
+
- Very easy to unit test
|
137
|
+
- Ability to filter out bad/junky data using [dry-validation](https://dry-rb.org/gems/dry-validation) contracts
|
138
|
+
|
139
|
+
## What are the main difference between this Ruby project and similar ones?
|
140
|
+
|
141
|
+
- Operates on a single entity rather than a list, so easily adoptable in existing systems
|
142
|
+
- Focuses on the "fetching" and filtering of data solely, and not transformation or storage
|
143
|
+
- Focuses on having a clean PORO interface to make testing simple
|
144
|
+
- Provides a declarative interface for merging results from many sources (APIs, legacy databases, etc.) which allows for prioritization
|
8
145
|
|
9
146
|
## Links
|
10
147
|
|
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] } } }
|
@@ -15,16 +15,16 @@ module Attr
|
|
15
15
|
@default = resolve(:deep_merge)
|
16
16
|
end
|
17
17
|
|
18
|
-
register(:deep_merge) do |*args|
|
18
|
+
register(:deep_merge) do |*args, **opts|
|
19
19
|
require 'attr/gather/aggregators/deep_merge'
|
20
20
|
|
21
|
-
DeepMerge.new(*args)
|
21
|
+
DeepMerge.new(*args, **opts)
|
22
22
|
end
|
23
23
|
|
24
|
-
register(:shallow_merge) do |*args|
|
24
|
+
register(:shallow_merge) do |*args, **opts|
|
25
25
|
require 'attr/gather/aggregators/shallow_merge'
|
26
26
|
|
27
|
-
ShallowMerge.new(*args)
|
27
|
+
ShallowMerge.new(*args, **opts)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'attr/gather/filters/noop'
|
4
|
+
|
3
5
|
module Attr
|
4
6
|
module Gather
|
5
7
|
module Aggregators
|
@@ -11,8 +13,14 @@ module Attr
|
|
11
13
|
class Base
|
12
14
|
attr_accessor :filter
|
13
15
|
|
16
|
+
NOOP_FILTER ||= Filters::Noop.new
|
17
|
+
|
14
18
|
def initialize(**opts)
|
15
|
-
@filter = opts.delete(:filter)
|
19
|
+
@filter = opts.delete(:filter) || NOOP_FILTER
|
20
|
+
end
|
21
|
+
|
22
|
+
def with(**opts)
|
23
|
+
self.class.new(filter: @filter, **opts)
|
16
24
|
end
|
17
25
|
|
18
26
|
def call(_original_input, _results_array)
|
@@ -21,16 +29,10 @@ module Attr
|
|
21
29
|
|
22
30
|
private
|
23
31
|
|
24
|
-
def wrap_result(result)
|
25
|
-
Concurrent::Promise.fulfill(result)
|
26
|
-
end
|
27
|
-
|
28
32
|
def unwrap_result(res)
|
29
|
-
|
30
|
-
|
31
|
-
return unvalidated if filter.nil?
|
33
|
+
return res if filter.nil?
|
32
34
|
|
33
|
-
filter.call(
|
35
|
+
filter.call(res).value
|
34
36
|
end
|
35
37
|
end
|
36
38
|
end
|
@@ -21,16 +21,19 @@ module Attr
|
|
21
21
|
|
22
22
|
def call(input, execution_results)
|
23
23
|
execution_results = execution_results.reverse_each if reverse?
|
24
|
+
initial = unwrap_initial_input(input)
|
24
25
|
|
25
|
-
|
26
|
+
execution_results.reduce(initial) do |memo, res|
|
26
27
|
deep_merge(memo, unwrap_result(res))
|
27
28
|
end
|
28
|
-
|
29
|
-
wrap_result(result)
|
30
29
|
end
|
31
30
|
|
32
31
|
private
|
33
32
|
|
33
|
+
def unwrap_initial_input(input)
|
34
|
+
input
|
35
|
+
end
|
36
|
+
|
34
37
|
def reverse?
|
35
38
|
@reverse
|
36
39
|
end
|
@@ -20,13 +20,11 @@ module Attr
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def call(input, execution_results)
|
23
|
-
|
23
|
+
execution_results = execution_results.reverse_each if reverse?
|
24
24
|
|
25
|
-
|
25
|
+
execution_results.reduce(input) do |memo, res|
|
26
26
|
memo.merge(unwrap_result(res))
|
27
27
|
end
|
28
|
-
|
29
|
-
wrap_result(result)
|
30
28
|
end
|
31
29
|
|
32
30
|
private
|
@@ -29,13 +29,13 @@ module Attr
|
|
29
29
|
# @param name [Symbol]
|
30
30
|
#
|
31
31
|
# @return [#call]
|
32
|
-
def resolve(name, *args)
|
32
|
+
def resolve(name, *args, **opts)
|
33
33
|
block = @__storage__.fetch(name) do
|
34
34
|
raise NotFoundError,
|
35
35
|
"no item with name #{name} registered"
|
36
36
|
end
|
37
37
|
|
38
|
-
block.call(*args)
|
38
|
+
block.call(*args, **opts)
|
39
39
|
end
|
40
40
|
|
41
41
|
# @api private
|
data/lib/attr/gather/version.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'attr/gather/workflow/task_executor'
|
4
|
-
require 'attr/gather/workflow/async_task_executor'
|
5
|
-
|
6
3
|
module Attr
|
7
4
|
module Gather
|
8
5
|
module Workflow
|
@@ -21,42 +18,39 @@ module Attr
|
|
21
18
|
#
|
22
19
|
# @param input [Hash]
|
23
20
|
#
|
24
|
-
# @return [Concurrent::Promise]
|
21
|
+
# @return [Concurrent::Promise<Hash>]
|
25
22
|
#
|
26
23
|
# @note For more information, check out {https://dry-rb.org/gems/dry-monads/1.0/result}
|
27
24
|
#
|
28
25
|
# @api public
|
29
26
|
def call(input)
|
30
|
-
|
27
|
+
task_promises = {}
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
final_results << executor_results
|
35
|
-
aggregator.call(aggregated_input, executor_results).value!
|
29
|
+
final_results = self.class.tasks.to_a.map do |task|
|
30
|
+
task_promises[task] = execute_task(input, task, task_promises)
|
36
31
|
end
|
37
32
|
|
38
|
-
|
33
|
+
Concurrent::Promise.zip(*final_results).then do |results|
|
34
|
+
aggregator.call(input, results)
|
35
|
+
end
|
39
36
|
end
|
40
37
|
|
41
38
|
private
|
42
39
|
|
43
|
-
# Enumator for task batches
|
44
|
-
#
|
45
|
-
# @return [Enumerator]
|
46
|
-
#
|
47
|
-
# @api private
|
48
|
-
def each_task_batch
|
49
|
-
self.class.tasks.each_batch
|
50
|
-
end
|
51
|
-
|
52
40
|
# Executes a batch of tasks
|
53
41
|
#
|
54
42
|
# @return [Array<TaskExecutionResult>]
|
55
43
|
#
|
56
44
|
# @api private
|
57
|
-
def
|
58
|
-
|
59
|
-
|
45
|
+
def execute_task(initial_input, task, task_promises)
|
46
|
+
task_proc = container.resolve(task.name)
|
47
|
+
dep_promises = task.depends_on.map { |t| task_promises[t] }
|
48
|
+
input_promise = Concurrent::Promise.zip(*dep_promises)
|
49
|
+
|
50
|
+
input_promise.then do |results|
|
51
|
+
dep_input = aggregator.call(initial_input, results)
|
52
|
+
task_proc.call(dep_input)
|
53
|
+
end
|
60
54
|
end
|
61
55
|
|
62
56
|
# @api private
|
@@ -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
|
#
|
@@ -36,12 +36,62 @@ module Attr
|
|
36
36
|
#
|
37
37
|
# @api public
|
38
38
|
def task(task_name, opts = EMPTY_HASH)
|
39
|
-
|
40
|
-
yield
|
41
|
-
tasks <<
|
39
|
+
conf = OpenStruct.new
|
40
|
+
yield conf
|
41
|
+
tasks << Hash[name: task_name, **opts, **conf.to_h]
|
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
|
@@ -88,13 +138,13 @@ module Attr
|
|
88
138
|
#
|
89
139
|
# @api public
|
90
140
|
def aggregator(agg = nil, opts = EMPTY_HASH)
|
91
|
-
if agg.nil? && !defined?(@aggregator)
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
141
|
+
@aggregator = if agg.nil? && !defined?(@aggregator)
|
142
|
+
Aggregators.default
|
143
|
+
elsif agg
|
144
|
+
Aggregators.resolve(agg, filter: filter, **opts)
|
145
|
+
else
|
146
|
+
@aggregator
|
147
|
+
end
|
98
148
|
end
|
99
149
|
|
100
150
|
# Defines a filter for filtering out invalid values
|
@@ -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.
|
@@ -130,12 +180,16 @@ module Attr
|
|
130
180
|
# @param args [Array<Object>] arguments for initializing the filter
|
131
181
|
#
|
132
182
|
# @api public
|
133
|
-
def filter(filt =
|
134
|
-
if filt
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
183
|
+
def filter(filt = nil, *args, **opts)
|
184
|
+
@filter = if filt.nil? && !defined?(@filter)
|
185
|
+
Filters.default
|
186
|
+
elsif filt
|
187
|
+
Filters.resolve(filt, *args, **opts)
|
188
|
+
else
|
189
|
+
@filter
|
190
|
+
end
|
191
|
+
|
192
|
+
aggregator.filter = @filter
|
139
193
|
|
140
194
|
@filter
|
141
195
|
end
|
@@ -147,7 +201,7 @@ module Attr
|
|
147
201
|
# @example
|
148
202
|
#
|
149
203
|
# class EnhanceUserProfile
|
150
|
-
#
|
204
|
+
# include Attr::Gather::Workflow
|
151
205
|
#
|
152
206
|
# # Any of the key/value pairs that had validation errors will be
|
153
207
|
# # filtered from the output.
|
@@ -169,7 +223,7 @@ module Attr
|
|
169
223
|
# @api public
|
170
224
|
def filter_with_contract(arg = nil, &blk)
|
171
225
|
contract = block_given? ? build_inline_contract_filter(&blk) : arg
|
172
|
-
|
226
|
+
filter(:contract, contract)
|
173
227
|
end
|
174
228
|
|
175
229
|
private
|
@@ -1,19 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'dry-equalizer'
|
4
|
+
|
3
5
|
module Attr
|
4
6
|
module Gather
|
5
7
|
module Workflow
|
6
8
|
# @api private
|
7
9
|
class Task
|
8
|
-
|
10
|
+
send :include, Dry::Equalizer(:name, :depends_on)
|
11
|
+
|
12
|
+
attr_accessor :name, :depends_on
|
9
13
|
|
14
|
+
# Initialize a new DeepMerge aggregator
|
15
|
+
#
|
16
|
+
# @param name [String] name of the task
|
17
|
+
# @param depends_on [Array<Task>] tasks needed before running this task
|
18
|
+
#
|
19
|
+
# @api private
|
10
20
|
def initialize(name:, depends_on: [])
|
11
21
|
@name = name
|
12
22
|
@depends_on = depends_on
|
13
23
|
end
|
14
24
|
|
25
|
+
# Check if this task depends on a given task
|
26
|
+
#
|
27
|
+
# @param other_task [Task] task to check
|
15
28
|
def depends_on?(other_task)
|
16
|
-
depends_on.include?(other_task
|
29
|
+
depends_on.include?(other_task)
|
17
30
|
end
|
18
31
|
|
19
32
|
def fullfilled_given_remaining_tasks?(task_list)
|
@@ -20,7 +20,9 @@ module Attr
|
|
20
20
|
tasks.each { |t| self << t }
|
21
21
|
end
|
22
22
|
|
23
|
-
def <<(
|
23
|
+
def <<(hash)
|
24
|
+
name, depends_on = hash.values_at :name, :depends_on
|
25
|
+
task = build_task(name, depends_on)
|
24
26
|
validate_for_insert!(task)
|
25
27
|
|
26
28
|
registered_tasks.each do |t|
|
@@ -68,6 +70,16 @@ module Attr
|
|
68
70
|
|
69
71
|
private
|
70
72
|
|
73
|
+
def build_task(name, depends_on)
|
74
|
+
deps = depends_on.map do |dep_name|
|
75
|
+
registered_tasks.find do |task|
|
76
|
+
task.name == dep_name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
Task.new(name: name, depends_on: deps)
|
81
|
+
end
|
82
|
+
|
71
83
|
def tsort_each_child(node, &blk)
|
72
84
|
to_h[node].each(&blk)
|
73
85
|
end
|
@@ -99,7 +111,7 @@ module Attr
|
|
99
111
|
end
|
100
112
|
|
101
113
|
def depended_on_tasks_exist?(task)
|
102
|
-
task.depends_on.all? { |t| registered_tasks.
|
114
|
+
task.depends_on.all? { |t| registered_tasks.include?(t) }
|
103
115
|
end
|
104
116
|
end
|
105
117
|
end
|
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.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ian Ker-Seymer
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -115,14 +115,11 @@ files:
|
|
115
115
|
- lib/attr/gather/filters/result.rb
|
116
116
|
- lib/attr/gather/version.rb
|
117
117
|
- lib/attr/gather/workflow.rb
|
118
|
-
- lib/attr/gather/workflow/async_task_executor.rb
|
119
118
|
- lib/attr/gather/workflow/callable.rb
|
120
119
|
- lib/attr/gather/workflow/dot_serializer.rb
|
121
120
|
- lib/attr/gather/workflow/dsl.rb
|
122
121
|
- lib/attr/gather/workflow/graphable.rb
|
123
122
|
- lib/attr/gather/workflow/task.rb
|
124
|
-
- lib/attr/gather/workflow/task_execution_result.rb
|
125
|
-
- lib/attr/gather/workflow/task_executor.rb
|
126
123
|
- lib/attr/gather/workflow/task_graph.rb
|
127
124
|
homepage: https://github.com/ianks/attr-gather
|
128
125
|
licenses:
|
@@ -130,7 +127,7 @@ licenses:
|
|
130
127
|
metadata:
|
131
128
|
homepage_uri: https://github.com/ianks/attr-gather
|
132
129
|
source_code_uri: https://github.com/ianks/attr-gather
|
133
|
-
post_install_message:
|
130
|
+
post_install_message:
|
134
131
|
rdoc_options: []
|
135
132
|
require_paths:
|
136
133
|
- lib
|
@@ -138,7 +135,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
138
135
|
requirements:
|
139
136
|
- - ">="
|
140
137
|
- !ruby/object:Gem::Version
|
141
|
-
version: '
|
138
|
+
version: '2.4'
|
142
139
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
140
|
requirements:
|
144
141
|
- - ">="
|
@@ -146,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
143
|
version: '0'
|
147
144
|
requirements: []
|
148
145
|
rubygems_version: 3.0.3
|
149
|
-
signing_key:
|
146
|
+
signing_key:
|
150
147
|
specification_version: 4
|
151
148
|
summary: Write a short summary, because RubyGems requires one.
|
152
149
|
test_files: []
|
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'attr/gather/workflow/task_executor'
|
4
|
-
|
5
|
-
module Attr
|
6
|
-
module Gather
|
7
|
-
module Workflow
|
8
|
-
# @api private
|
9
|
-
class AsyncTaskExecutor < TaskExecutor
|
10
|
-
def initialize(*)
|
11
|
-
super
|
12
|
-
@executor = :io
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,58 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Attr
|
4
|
-
module Gather
|
5
|
-
module Workflow
|
6
|
-
# A wrapper containing information and results of a task execution
|
7
|
-
#
|
8
|
-
# @!attribute [r] started_at
|
9
|
-
# @return [Time] time which the execution occured
|
10
|
-
#
|
11
|
-
# @!attribute [r] task
|
12
|
-
# @return [Attr::Gather::Workflow::Task] task that was run
|
13
|
-
#
|
14
|
-
# @!attribute [r] result
|
15
|
-
# @return [Concurrent::Promise] the result promise of the the task
|
16
|
-
#
|
17
|
-
# @api public
|
18
|
-
class TaskExecutionResult
|
19
|
-
include Concerns::Identifiable
|
20
|
-
|
21
|
-
attr_reader :task, :result, :started_at, :uuid
|
22
|
-
|
23
|
-
def initialize(task, result)
|
24
|
-
@started_at = Time.now
|
25
|
-
@uuid = SecureRandom.uuid
|
26
|
-
@task = task
|
27
|
-
@result = result
|
28
|
-
end
|
29
|
-
|
30
|
-
# @!attribute [r] state
|
31
|
-
# @return [:unscheduled, :pending, :processing, :rejected, :fulfilled]
|
32
|
-
def state
|
33
|
-
result.state
|
34
|
-
end
|
35
|
-
|
36
|
-
# Extracts the result, this is an unsafe operation that blocks the
|
37
|
-
# operation, and returns either the value or an exception.
|
38
|
-
#
|
39
|
-
# @note For more information, check out {https://ruby-concurrency.github.io/concurrent-ruby/1.1.5/Concurrent/Concern/Obligation.html#value!-instance_method}
|
40
|
-
def value!
|
41
|
-
result.value!
|
42
|
-
end
|
43
|
-
|
44
|
-
# Represents the TaskExecutionResult as a hash
|
45
|
-
#
|
46
|
-
# @return [Hash]
|
47
|
-
def as_json
|
48
|
-
value = result.value
|
49
|
-
|
50
|
-
{ started_at: started_at,
|
51
|
-
task: task.as_json,
|
52
|
-
state: state,
|
53
|
-
value: value }
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'concurrent'
|
4
|
-
require 'attr/gather/workflow/task_execution_result'
|
5
|
-
|
6
|
-
module Attr
|
7
|
-
module Gather
|
8
|
-
module Workflow
|
9
|
-
# @api private
|
10
|
-
class TaskExecutor
|
11
|
-
attr_reader :batch, :container, :executor
|
12
|
-
|
13
|
-
def initialize(batch, container:)
|
14
|
-
@batch = batch
|
15
|
-
@container = container
|
16
|
-
@executor = :immediate
|
17
|
-
end
|
18
|
-
|
19
|
-
def call(input)
|
20
|
-
batch.map do |task|
|
21
|
-
task_proc = container.resolve(task.name)
|
22
|
-
result = Concurrent::Promise.execute(executor: executor) do
|
23
|
-
task_proc.call(input)
|
24
|
-
end
|
25
|
-
TaskExecutionResult.new(task, result)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|