attr-gather 1.1.3 → 1.5.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/doc.yml +1 -1
- data/.github/workflows/ruby.yml +3 -0
- data/.rubocop.yml +2 -1
- data/.ruby-version +1 -1
- data/Gemfile.lock +69 -60
- data/README.md +142 -4
- data/Rakefile +1 -1
- data/attr-gather.gemspec +2 -0
- data/examples/post_enhancer.rb +1 -1
- data/lib/attr/gather/aggregators.rb +1 -0
- data/lib/attr/gather/aggregators/base.rb +7 -9
- data/lib/attr/gather/aggregators/deep_merge.rb +33 -18
- data/lib/attr/gather/aggregators/shallow_merge.rb +2 -13
- data/lib/attr/gather/filters/contract.rb +1 -1
- data/lib/attr/gather/filters/noop.rb +2 -0
- data/lib/attr/gather/version.rb +1 -1
- data/lib/attr/gather/workflow/callable.rb +18 -22
- data/lib/attr/gather/workflow/dot_serializer.rb +1 -2
- data/lib/attr/gather/workflow/dsl.rb +68 -9
- data/lib/attr/gather/workflow/task.rb +15 -2
- data/lib/attr/gather/workflow/task_graph.rb +15 -2
- metadata +3 -6
- data/lib/attr/gather/workflow/async_task_executor.rb +0 -17
- data/lib/attr/gather/workflow/task_execution_result.rb +0 -77
- 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: d5a781371ace4374427a2510da9860b10014caf039fcfd4aa620886f252d57a9
|
|
4
|
+
data.tar.gz: a0d1d564f206b30246e67d017e8a814c43529b120c6ff8499ef0198d5847ad7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52823ab8e9b562de9b23b8bdbe6bc08354bb85f71bd51580ab746fd4adce30cade4d27d55c3bf67a294bbb101f61eae485bc006537c89aefcba57ca28f3e8e39
|
|
7
|
+
data.tar.gz: '0998be6201c8b8292297390064233d0ad04c17b5b8d84ceebf76f0766e5d84fdfafb7edc0d2a4621825f2efafd66b192f89a3fbd38d6462b16ecbecbea7abc01'
|
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/doc.yml
CHANGED
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
bundle install --without=local --jobs 4 --retry 3
|
|
17
17
|
bundle exec yard
|
|
18
18
|
- name: Deploy Docs
|
|
19
|
-
uses: JamesIves/github-pages-deploy-action@
|
|
19
|
+
uses: JamesIves/github-pages-deploy-action@4.1.3
|
|
20
20
|
if: github.ref == 'master'
|
|
21
21
|
env:
|
|
22
22
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
data/.github/workflows/ruby.yml
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -5,6 +5,7 @@ AllCops:
|
|
|
5
5
|
UseCache: true
|
|
6
6
|
CacheRootDirectory: './tmp/cache'
|
|
7
7
|
TargetRubyVersion: 2.5
|
|
8
|
+
NewCops: enable
|
|
8
9
|
|
|
9
10
|
Metrics/BlockLength:
|
|
10
11
|
Exclude:
|
|
@@ -14,7 +15,7 @@ Metrics/ModuleLength:
|
|
|
14
15
|
Exclude:
|
|
15
16
|
- spec/**/*.rb
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Layout/LineLength:
|
|
18
19
|
Exclude:
|
|
19
20
|
- ./attr-gather.gemspec
|
|
20
21
|
- ./bin/**/*
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.6.
|
|
1
|
+
2.6.6
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
attr-gather (1.1
|
|
4
|
+
attr-gather (1.5.1)
|
|
5
5
|
dry-container (~> 0.7)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -9,46 +9,42 @@ 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.2)
|
|
13
13
|
backport (1.1.2)
|
|
14
|
-
benchmark (0.1.
|
|
14
|
+
benchmark (0.1.1)
|
|
15
15
|
coderay (1.1.3)
|
|
16
|
-
concurrent-ruby (1.1.
|
|
17
|
-
diff-lcs (1.
|
|
16
|
+
concurrent-ruby (1.1.8)
|
|
17
|
+
diff-lcs (1.4.4)
|
|
18
18
|
domain_name (0.5.20190701)
|
|
19
19
|
unf (>= 0.0.5, < 1.0.0)
|
|
20
|
-
dry-configurable (0.
|
|
20
|
+
dry-configurable (0.12.1)
|
|
21
21
|
concurrent-ruby (~> 1.0)
|
|
22
|
-
dry-core (~> 0.
|
|
23
|
-
dry-equalizer (~> 0.2)
|
|
22
|
+
dry-core (~> 0.5, >= 0.5.0)
|
|
24
23
|
dry-container (0.7.2)
|
|
25
24
|
concurrent-ruby (~> 1.0)
|
|
26
25
|
dry-configurable (~> 0.1, >= 0.1.3)
|
|
27
|
-
dry-core (0.
|
|
26
|
+
dry-core (0.6.0)
|
|
28
27
|
concurrent-ruby (~> 1.0)
|
|
29
28
|
dry-equalizer (0.3.0)
|
|
30
29
|
dry-inflector (0.2.0)
|
|
31
30
|
dry-initializer (3.0.4)
|
|
32
|
-
dry-logic (1.0
|
|
31
|
+
dry-logic (1.2.0)
|
|
33
32
|
concurrent-ruby (~> 1.0)
|
|
34
|
-
dry-core (~> 0.
|
|
35
|
-
|
|
36
|
-
dry-schema (1.5.5)
|
|
33
|
+
dry-core (~> 0.5, >= 0.5)
|
|
34
|
+
dry-schema (1.6.2)
|
|
37
35
|
concurrent-ruby (~> 1.0)
|
|
38
36
|
dry-configurable (~> 0.8, >= 0.8.3)
|
|
39
|
-
dry-core (~> 0.
|
|
40
|
-
dry-equalizer (~> 0.2)
|
|
37
|
+
dry-core (~> 0.5, >= 0.5)
|
|
41
38
|
dry-initializer (~> 3.0)
|
|
42
39
|
dry-logic (~> 1.0)
|
|
43
|
-
dry-types (~> 1.
|
|
44
|
-
dry-types (1.
|
|
40
|
+
dry-types (~> 1.5)
|
|
41
|
+
dry-types (1.5.1)
|
|
45
42
|
concurrent-ruby (~> 1.0)
|
|
46
43
|
dry-container (~> 0.3)
|
|
47
|
-
dry-core (~> 0.
|
|
48
|
-
dry-equalizer (~> 0.3)
|
|
44
|
+
dry-core (~> 0.5, >= 0.5)
|
|
49
45
|
dry-inflector (~> 0.1, >= 0.1.2)
|
|
50
46
|
dry-logic (~> 1.0, >= 1.0.2)
|
|
51
|
-
dry-validation (1.
|
|
47
|
+
dry-validation (1.6.0)
|
|
52
48
|
concurrent-ruby (~> 1.0)
|
|
53
49
|
dry-container (~> 0.7, >= 0.7.1)
|
|
54
50
|
dry-core (~> 0.4)
|
|
@@ -56,81 +52,94 @@ GEM
|
|
|
56
52
|
dry-initializer (~> 3.0)
|
|
57
53
|
dry-schema (~> 1.5, >= 1.5.2)
|
|
58
54
|
e2mmap (0.1.0)
|
|
59
|
-
ffi (1.
|
|
55
|
+
ffi (1.15.1)
|
|
60
56
|
ffi-compiler (1.0.1)
|
|
61
57
|
ffi (>= 1.0.0)
|
|
62
58
|
rake
|
|
63
|
-
http (
|
|
59
|
+
http (5.0.0)
|
|
64
60
|
addressable (~> 2.3)
|
|
65
61
|
http-cookie (~> 1.0)
|
|
66
62
|
http-form_data (~> 2.2)
|
|
67
|
-
|
|
63
|
+
llhttp-ffi (~> 0.0.1)
|
|
68
64
|
http-cookie (1.0.3)
|
|
69
65
|
domain_name (~> 0.5)
|
|
70
66
|
http-form_data (2.3.0)
|
|
71
|
-
http-parser (1.2.1)
|
|
72
|
-
ffi-compiler (>= 1.0, < 2.0)
|
|
73
67
|
jaro_winkler (1.5.4)
|
|
74
|
-
|
|
68
|
+
kramdown (2.3.1)
|
|
69
|
+
rexml
|
|
70
|
+
kramdown-parser-gfm (1.1.0)
|
|
71
|
+
kramdown (~> 2.0)
|
|
72
|
+
llhttp-ffi (0.0.1)
|
|
73
|
+
ffi-compiler (~> 1.0)
|
|
74
|
+
rake (~> 13.0)
|
|
75
75
|
method_source (1.0.0)
|
|
76
|
-
mini_portile2 (2.
|
|
77
|
-
nokogiri (1.
|
|
78
|
-
mini_portile2 (~> 2.
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
mini_portile2 (2.5.3)
|
|
77
|
+
nokogiri (1.11.7)
|
|
78
|
+
mini_portile2 (~> 2.5.0)
|
|
79
|
+
racc (~> 1.4)
|
|
80
|
+
parallel (1.20.1)
|
|
81
|
+
parser (3.0.1.1)
|
|
81
82
|
ast (~> 2.4.1)
|
|
82
|
-
pry (0.
|
|
83
|
+
pry (0.14.1)
|
|
83
84
|
coderay (~> 1.1)
|
|
84
85
|
method_source (~> 1.0)
|
|
85
86
|
public_suffix (4.0.6)
|
|
87
|
+
racc (1.5.2)
|
|
86
88
|
rainbow (3.0.0)
|
|
87
|
-
rake (13.0.
|
|
89
|
+
rake (13.0.3)
|
|
90
|
+
regexp_parser (2.1.1)
|
|
88
91
|
reverse_markdown (2.0.0)
|
|
89
92
|
nokogiri
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
rspec-
|
|
93
|
-
rspec-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
rexml (3.2.5)
|
|
94
|
+
rspec (3.10.0)
|
|
95
|
+
rspec-core (~> 3.10.0)
|
|
96
|
+
rspec-expectations (~> 3.10.0)
|
|
97
|
+
rspec-mocks (~> 3.10.0)
|
|
98
|
+
rspec-core (3.10.1)
|
|
99
|
+
rspec-support (~> 3.10.0)
|
|
100
|
+
rspec-expectations (3.10.1)
|
|
97
101
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
98
|
-
rspec-support (~> 3.
|
|
99
|
-
rspec-mocks (3.
|
|
102
|
+
rspec-support (~> 3.10.0)
|
|
103
|
+
rspec-mocks (3.10.2)
|
|
100
104
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
101
|
-
rspec-support (~> 3.
|
|
102
|
-
rspec-support (3.
|
|
103
|
-
rubocop (
|
|
104
|
-
jaro_winkler (~> 1.5.1)
|
|
105
|
+
rspec-support (~> 3.10.0)
|
|
106
|
+
rspec-support (3.10.2)
|
|
107
|
+
rubocop (1.16.0)
|
|
105
108
|
parallel (~> 1.10)
|
|
106
|
-
parser (>=
|
|
109
|
+
parser (>= 3.0.0.0)
|
|
107
110
|
rainbow (>= 2.2.2, < 4.0)
|
|
111
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
112
|
+
rexml
|
|
113
|
+
rubocop-ast (>= 1.7.0, < 2.0)
|
|
108
114
|
ruby-progressbar (~> 1.7)
|
|
109
|
-
unicode-display_width (>= 1.4.0, <
|
|
110
|
-
rubocop-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
|
116
|
+
rubocop-ast (1.7.0)
|
|
117
|
+
parser (>= 3.0.1.1)
|
|
118
|
+
rubocop-performance (1.11.3)
|
|
119
|
+
rubocop (>= 1.7.0, < 2.0)
|
|
120
|
+
rubocop-ast (>= 0.4.0)
|
|
121
|
+
ruby-progressbar (1.11.0)
|
|
122
|
+
solargraph (0.41.1)
|
|
114
123
|
backport (~> 1.1)
|
|
115
124
|
benchmark
|
|
116
125
|
bundler (>= 1.17.2)
|
|
117
126
|
e2mmap
|
|
118
127
|
jaro_winkler (~> 1.5)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
parser (~>
|
|
128
|
+
kramdown (~> 2.3)
|
|
129
|
+
kramdown-parser-gfm (~> 1.1)
|
|
130
|
+
parser (~> 3.0)
|
|
122
131
|
reverse_markdown (>= 1.0.5, < 3)
|
|
123
|
-
rubocop (
|
|
132
|
+
rubocop (>= 0.52)
|
|
124
133
|
thor (~> 1.0)
|
|
125
134
|
tilt (~> 2.0)
|
|
126
135
|
yard (~> 0.9, >= 0.9.24)
|
|
127
|
-
thor (1.0
|
|
136
|
+
thor (1.1.0)
|
|
128
137
|
tilt (2.0.10)
|
|
129
138
|
unf (0.1.4)
|
|
130
139
|
unf_ext
|
|
131
140
|
unf_ext (0.0.7.7)
|
|
132
|
-
unicode-display_width (
|
|
133
|
-
yard (0.9.
|
|
141
|
+
unicode-display_width (2.0.0)
|
|
142
|
+
yard (0.9.26)
|
|
134
143
|
|
|
135
144
|
PLATFORMS
|
|
136
145
|
ruby
|
|
@@ -149,4 +158,4 @@ DEPENDENCIES
|
|
|
149
158
|
yard
|
|
150
159
|
|
|
151
160
|
BUNDLED WITH
|
|
152
|
-
2.
|
|
161
|
+
2.2.11
|
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/Rakefile
CHANGED
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.5'
|
|
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] } } }
|
|
@@ -13,28 +13,26 @@ module Attr
|
|
|
13
13
|
class Base
|
|
14
14
|
attr_accessor :filter
|
|
15
15
|
|
|
16
|
-
NOOP_FILTER
|
|
16
|
+
NOOP_FILTER = Filters::Noop.new
|
|
17
17
|
|
|
18
18
|
def initialize(**opts)
|
|
19
19
|
@filter = opts.delete(:filter) || NOOP_FILTER
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def with(**opts)
|
|
23
|
+
self.class.new(filter: @filter, **opts)
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
def call(_original_input, _results_array)
|
|
23
27
|
raise NotImplementedError
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
private
|
|
27
31
|
|
|
28
|
-
def wrap_result(result)
|
|
29
|
-
Concurrent::Promise.fulfill(result)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
32
|
def unwrap_result(res)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return unvalidated if filter.nil?
|
|
33
|
+
return res if filter.nil?
|
|
36
34
|
|
|
37
|
-
filter.call(
|
|
35
|
+
filter.call(res).value
|
|
38
36
|
end
|
|
39
37
|
end
|
|
40
38
|
end
|
|
@@ -12,49 +12,64 @@ 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]
|
|
15
|
+
# @param merge_input [Boolean] merge the result with the initial input
|
|
16
|
+
# @param array_strategy [Symbol] strategy for handling arrays, one of (:concat, :overwrite)
|
|
16
17
|
#
|
|
17
18
|
# @api private
|
|
18
|
-
def initialize(reverse: false, merge_input: true, **)
|
|
19
|
+
def initialize(reverse: false, merge_input: true, array_strategy: :concat, **)
|
|
20
|
+
unless ARRAY_STRATEGY.include?(array_strategy)
|
|
21
|
+
raise ArgumentError, 'array_strategy must be one of: :concat, :overwrite'
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
@reverse = reverse
|
|
20
25
|
@merge_input = merge_input
|
|
26
|
+
@array_strategy = array_strategy
|
|
27
|
+
|
|
21
28
|
super
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
def call(input, execution_results)
|
|
25
32
|
execution_results = execution_results.reverse_each if reverse?
|
|
26
|
-
initial = unwrap_initial_input(input)
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
execution_results.reduce(@merge_input ? input : EMPTY_HASH) do |memo, res|
|
|
29
35
|
deep_merge(memo, unwrap_result(res))
|
|
30
36
|
end
|
|
31
|
-
|
|
32
|
-
wrap_result(result)
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
private
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
merge_input? ? filter.call(input.dup).value : {}
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def reverse?
|
|
42
|
-
@reverse
|
|
43
|
-
end
|
|
41
|
+
ARRAY_STRATEGY = %i[concat overwrite].freeze
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
@merge_input
|
|
47
|
-
end
|
|
43
|
+
private_constant :ARRAY_STRATEGY
|
|
48
44
|
|
|
49
45
|
def deep_merge(hash, other)
|
|
50
|
-
|
|
46
|
+
hash.to_h.merge(other) do |_, orig, new|
|
|
51
47
|
if orig.respond_to?(:to_hash) && new.respond_to?(:to_hash)
|
|
52
|
-
deep_merge(
|
|
48
|
+
deep_merge(orig.to_h, new.to_h)
|
|
49
|
+
elsif concattable?(orig, new)
|
|
50
|
+
orig + new
|
|
53
51
|
else
|
|
54
52
|
new
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
end
|
|
56
|
+
|
|
57
|
+
def concattable?(orig, new)
|
|
58
|
+
return false unless @array_strategy == :concat
|
|
59
|
+
|
|
60
|
+
concattable_class?(orig) && concattable_class?(new)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def concattable_class?(obj)
|
|
64
|
+
return true if obj.is_a?(Array)
|
|
65
|
+
return true if obj.is_a?(Set)
|
|
66
|
+
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reverse?
|
|
71
|
+
@reverse
|
|
72
|
+
end
|
|
58
73
|
end
|
|
59
74
|
end
|
|
60
75
|
end
|
|
@@ -12,7 +12,7 @@ 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]
|
|
15
|
+
# @param merge_input [Boolean] merge the result with the initial input
|
|
16
16
|
#
|
|
17
17
|
# @api private
|
|
18
18
|
def initialize(reverse: false, merge_input: true, **)
|
|
@@ -23,28 +23,17 @@ module Attr
|
|
|
23
23
|
|
|
24
24
|
def call(input, execution_results)
|
|
25
25
|
execution_results = execution_results.reverse_each if reverse?
|
|
26
|
-
initial = unwrap_initial_input(input)
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
execution_results.reduce(@merge_input ? input : EMPTY_HASH) do |memo, res|
|
|
29
28
|
memo.merge(unwrap_result(res))
|
|
30
29
|
end
|
|
31
|
-
|
|
32
|
-
wrap_result(result)
|
|
33
30
|
end
|
|
34
31
|
|
|
35
32
|
private
|
|
36
33
|
|
|
37
|
-
def unwrap_initial_input(input)
|
|
38
|
-
merge_input? ? filter.call(input.dup).value : {}
|
|
39
|
-
end
|
|
40
|
-
|
|
41
34
|
def reverse?
|
|
42
35
|
@reverse
|
|
43
36
|
end
|
|
44
|
-
|
|
45
|
-
def merge_input?
|
|
46
|
-
@merge_input
|
|
47
|
-
end
|
|
48
37
|
end
|
|
49
38
|
end
|
|
50
39
|
end
|
data/lib/attr/gather/version.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
4
|
-
require 'attr/gather/workflow/async_task_executor'
|
|
3
|
+
require 'concurrent/promise'
|
|
5
4
|
|
|
6
5
|
module Attr
|
|
7
6
|
module Gather
|
|
@@ -21,42 +20,39 @@ module Attr
|
|
|
21
20
|
#
|
|
22
21
|
# @param input [Hash]
|
|
23
22
|
#
|
|
24
|
-
# @return [Concurrent::Promise]
|
|
23
|
+
# @return [Concurrent::Promise<Hash>]
|
|
25
24
|
#
|
|
26
25
|
# @note For more information, check out {https://dry-rb.org/gems/dry-monads/1.0/result}
|
|
27
26
|
#
|
|
28
27
|
# @api public
|
|
29
28
|
def call(input)
|
|
30
|
-
|
|
29
|
+
task_promises = {}
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
final_results << executor_results
|
|
35
|
-
aggregator.call(aggregated_input, executor_results).value!
|
|
31
|
+
final_results = self.class.tasks.to_a.map do |task|
|
|
32
|
+
task_promises[task] = execute_task(input, task, task_promises)
|
|
36
33
|
end
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
Concurrent::Promise.zip(*final_results).then do |results|
|
|
36
|
+
aggregator.call(input, results)
|
|
37
|
+
end
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
private
|
|
42
41
|
|
|
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
42
|
# Executes a batch of tasks
|
|
53
43
|
#
|
|
54
44
|
# @return [Array<TaskExecutionResult>]
|
|
55
45
|
#
|
|
56
46
|
# @api private
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
def execute_task(initial_input, task, task_promises)
|
|
48
|
+
task_proc = container.resolve(task.name)
|
|
49
|
+
dep_promises = task.depends_on.map { |t| task_promises[t] }
|
|
50
|
+
input_promise = Concurrent::Promise.zip(*dep_promises)
|
|
51
|
+
|
|
52
|
+
input_promise.then do |results|
|
|
53
|
+
dep_input = aggregator.call(initial_input, results)
|
|
54
|
+
task_proc.call(dep_input)
|
|
55
|
+
end
|
|
60
56
|
end
|
|
61
57
|
|
|
62
58
|
# @api private
|
|
@@ -69,7 +65,7 @@ module Attr
|
|
|
69
65
|
return @aggregator if defined?(@aggregator) && !@aggregator.nil?
|
|
70
66
|
|
|
71
67
|
@aggregator = self.class.aggregator
|
|
72
|
-
@aggregator.filter ||= filter
|
|
68
|
+
@aggregator.filter ||= filter if @aggregator.respond_to?(:filter=)
|
|
73
69
|
|
|
74
70
|
@aggregator
|
|
75
71
|
end
|
|
@@ -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 << ({ 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,17 +129,26 @@ 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
|
|
86
136
|
#
|
|
137
|
+
# @example
|
|
138
|
+
# class EnhanceUserProfile
|
|
139
|
+
# include Attr::Gather::Workflow
|
|
140
|
+
#
|
|
141
|
+
# aggregator MyCustomAggregator
|
|
142
|
+
# end
|
|
143
|
+
#
|
|
87
144
|
# @param agg [#call] the aggregator to use
|
|
88
145
|
#
|
|
89
146
|
# @api public
|
|
90
147
|
def aggregator(agg = nil, opts = EMPTY_HASH)
|
|
91
148
|
@aggregator = if agg.nil? && !defined?(@aggregator)
|
|
92
149
|
Aggregators.default
|
|
150
|
+
elsif agg.respond_to?(:new)
|
|
151
|
+
agg.new(filter: filter, **opts)
|
|
93
152
|
elsif agg
|
|
94
153
|
Aggregators.resolve(agg, filter: filter, **opts)
|
|
95
154
|
else
|
|
@@ -119,7 +178,7 @@ module Attr
|
|
|
119
178
|
# end
|
|
120
179
|
#
|
|
121
180
|
# class EnhanceUserProfile
|
|
122
|
-
#
|
|
181
|
+
# include Attr::Gather::Workflow
|
|
123
182
|
#
|
|
124
183
|
# # Any of the key/value pairs that had validation errors will be
|
|
125
184
|
# # filtered from the output.
|
|
@@ -151,7 +210,7 @@ module Attr
|
|
|
151
210
|
# @example
|
|
152
211
|
#
|
|
153
212
|
# class EnhanceUserProfile
|
|
154
|
-
#
|
|
213
|
+
# include Attr::Gather::Workflow
|
|
155
214
|
#
|
|
156
215
|
# # Any of the key/value pairs that had validation errors will be
|
|
157
216
|
# # filtered from the output.
|
|
@@ -172,7 +231,7 @@ module Attr
|
|
|
172
231
|
#
|
|
173
232
|
# @api public
|
|
174
233
|
def filter_with_contract(arg = nil, &blk)
|
|
175
|
-
contract =
|
|
234
|
+
contract = blk ? build_inline_contract_filter(&blk) : arg
|
|
176
235
|
filter(:contract, contract)
|
|
177
236
|
end
|
|
178
237
|
|
|
@@ -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)
|
|
@@ -9,6 +9,7 @@ module Attr
|
|
|
9
9
|
# @api private
|
|
10
10
|
class TaskGraph
|
|
11
11
|
class UnfinishableError < StandardError; end
|
|
12
|
+
|
|
12
13
|
class InvalidTaskDepedencyError < StandardError; end
|
|
13
14
|
|
|
14
15
|
include TSort
|
|
@@ -20,7 +21,9 @@ module Attr
|
|
|
20
21
|
tasks.each { |t| self << t }
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
def <<(
|
|
24
|
+
def <<(hash)
|
|
25
|
+
name, depends_on = hash.values_at :name, :depends_on
|
|
26
|
+
task = build_task(name, depends_on)
|
|
24
27
|
validate_for_insert!(task)
|
|
25
28
|
|
|
26
29
|
registered_tasks.each do |t|
|
|
@@ -68,6 +71,16 @@ module Attr
|
|
|
68
71
|
|
|
69
72
|
private
|
|
70
73
|
|
|
74
|
+
def build_task(name, depends_on)
|
|
75
|
+
deps = depends_on.map do |dep_name|
|
|
76
|
+
registered_tasks.find do |task|
|
|
77
|
+
task.name == dep_name
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
Task.new(name: name, depends_on: deps)
|
|
82
|
+
end
|
|
83
|
+
|
|
71
84
|
def tsort_each_child(node, &blk)
|
|
72
85
|
to_h[node].each(&blk)
|
|
73
86
|
end
|
|
@@ -99,7 +112,7 @@ module Attr
|
|
|
99
112
|
end
|
|
100
113
|
|
|
101
114
|
def depended_on_tasks_exist?(task)
|
|
102
|
-
task.depends_on.all? { |t| registered_tasks.
|
|
115
|
+
task.depends_on.all? { |t| registered_tasks.include?(t) }
|
|
103
116
|
end
|
|
104
117
|
end
|
|
105
118
|
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.1
|
|
4
|
+
version: 1.5.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-06-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:
|
|
@@ -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.5'
|
|
142
139
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
140
|
requirements:
|
|
144
141
|
- - ">="
|
|
@@ -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(batch, container:)
|
|
11
|
-
super(batch, container: container)
|
|
12
|
-
@executor = :io
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
@@ -1,77 +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, started_at: Time.now, uuid: SecureRandom.uuid) # rubocop:disable Metrics/LineLength
|
|
24
|
-
@task = task
|
|
25
|
-
@result = result
|
|
26
|
-
@started_at = started_at
|
|
27
|
-
@uuid = uuid
|
|
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
|
-
# Chain a new block result to be executed after resolution
|
|
45
|
-
#
|
|
46
|
-
# @return [TaskExecutionResult] the new task execution result
|
|
47
|
-
# @yield The block operation to be performed asynchronously.
|
|
48
|
-
def then(*args, &block)
|
|
49
|
-
new_result = result.then(*args, &block)
|
|
50
|
-
self.class.new(task, new_result, started_at: @started_at, uuid: @uuid)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Catch an async exception when a failure occurs
|
|
54
|
-
#
|
|
55
|
-
# @return [TaskExecutionResult] the new task execution result
|
|
56
|
-
# @yield The block operation to be performed asynchronously.
|
|
57
|
-
def catch(*args, &block)
|
|
58
|
-
new_result = result.catch(*args, &block)
|
|
59
|
-
self.class.new(task, new_result, started_at: @started_at, uuid: @uuid)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Executes a block after the result is fulfilled
|
|
63
|
-
# Represents the TaskExecutionResult as a hash
|
|
64
|
-
#
|
|
65
|
-
# @return [Hash]
|
|
66
|
-
def as_json
|
|
67
|
-
value = result.value
|
|
68
|
-
|
|
69
|
-
{ started_at: started_at,
|
|
70
|
-
task: task.as_json,
|
|
71
|
-
state: state,
|
|
72
|
-
value: value }
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
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
|