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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90807fd9b5c605b23dff88c04cb4355fb6ad7ae33802217317db57440d77fbb3
4
- data.tar.gz: 2cfd59a2a602caf2184c6baec506b73304d5cfcf451d4697b6d2d854601201f8
3
+ metadata.gz: edfd2d7479248348551d31676b45459f6f8279f6ca5fdf6ed00540c791de86bd
4
+ data.tar.gz: 977ac39f355ded8101507de998a221354ba357bad66528f9efbbb50200d4d731
5
5
  SHA512:
6
- metadata.gz: 6fe28e663ad8c84187e738a5fecfebd99c2f3d4ac62b0df66331fed31be79fb753d7cdc1ce6639048c80035669757b4cdcb67f7ac7b03d266703707db229a6a7
7
- data.tar.gz: 75929007743f4347561d869dcfbf0cfbc9b8972cc0a79691a32e037bb681fabb2a6f90b4a7dcf5ee1ec1691592b0f2630eff37a06dc6fd081cc1b99fbd52a3e4
6
+ metadata.gz: 958601e4741c30c6707ee6808fb89a258dfa10bab373f623d63fed29faef5eaf0d54d8fecfba4e9949bd16afb7d23bd84aca80aa2d485243de84076e2087284a
7
+ data.tar.gz: 11d1ae7d926743a202413647e1fe0f75e3c47966c9444c83a25e706ab8b923fa37ea2cda565042634d0cf6abc900be5fa2e1d3b47c436e6efc950a5d6f3d8fde
@@ -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=build-test-lint"
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
@@ -5,7 +5,7 @@ on:
5
5
  tags:
6
6
  - v*
7
7
  jobs:
8
- build-test-lint:
8
+ deploy:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - uses: actions/checkout@v1
@@ -4,6 +4,9 @@ on: [push]
4
4
  jobs:
5
5
  build-test-lint:
6
6
  runs-on: ubuntu-latest
7
+ strategy:
8
+ matrix:
9
+ ruby: ["2.4.x", "2.5.x", "2.6.x", "2.7.x"]
7
10
  steps:
8
11
  - uses: actions/checkout@v1
9
12
  - name: Set up Ruby 2.6
@@ -4,7 +4,8 @@ require:
4
4
  AllCops:
5
5
  UseCache: true
6
6
  CacheRootDirectory: './tmp/cache'
7
- TargetRubyVersion: 2.5
7
+ TargetRubyVersion: 2.4
8
+ NewCops: enable
8
9
 
9
10
  Metrics/BlockLength:
10
11
  Exclude:
data/Gemfile CHANGED
@@ -6,7 +6,7 @@ source 'https://rubygems.org'
6
6
  gemspec
7
7
 
8
8
  group :development, :test do
9
- gem 'dry-validation', '~> 1.3'
9
+ gem 'dry-validation', '~> 1.5'
10
10
  gem 'pry'
11
11
  gem 'rubocop'
12
12
  gem 'rubocop-performance'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- attr-gather (1.1.0)
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.0)
12
+ ast (2.4.1)
13
13
  backport (1.1.2)
14
- coderay (1.1.2)
15
- concurrent-ruby (1.1.5)
16
- diff-lcs (1.3)
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.8.3)
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.2.2)
29
+ dry-equalizer (0.3.0)
28
30
  dry-inflector (0.2.0)
29
- dry-initializer (3.0.1)
30
- dry-logic (1.0.3)
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.4.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.2)
42
- dry-types (1.2.0)
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.2, >= 0.2.2)
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.3.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.0, >= 1.3.1)
56
- ffi (1.11.1)
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
- htmlentities (4.3.4)
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.0)
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.1.1)
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.3)
72
- method_source (0.9.2)
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.8)
77
+ nokogiri (1.10.10)
75
78
  mini_portile2 (~> 2.4.0)
76
- parallel (1.18.0)
77
- parser (2.6.5.0)
78
- ast (~> 2.4.0)
79
- pry (0.12.2)
80
- coderay (~> 1.1.0)
81
- method_source (~> 0.9.0)
82
- public_suffix (4.0.1)
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
- reverse_markdown (1.3.0)
88
+ regexp_parser (2.0.0)
89
+ reverse_markdown (2.0.0)
86
90
  nokogiri
87
- rspec (3.9.0)
88
- rspec-core (~> 3.9.0)
89
- rspec-expectations (~> 3.9.0)
90
- rspec-mocks (~> 3.9.0)
91
- rspec-core (3.9.0)
92
- rspec-support (~> 3.9.0)
93
- rspec-expectations (3.9.0)
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.9.0)
96
- rspec-mocks (3.9.0)
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.9.0)
99
- rspec-support (3.9.0)
100
- rubocop (0.75.1)
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.6)
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, < 1.7)
107
- rubocop-performance (1.5.1)
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.37.2)
119
+ solargraph (0.39.17)
111
120
  backport (~> 1.1)
121
+ benchmark
112
122
  bundler (>= 1.17.2)
113
- htmlentities (~> 4.3, >= 4.3.4)
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 (~> 1.0, >= 1.0.5)
128
+ reverse_markdown (>= 1.0.5, < 3)
118
129
  rubocop (~> 0.52)
119
- thor (~> 0.19, >= 0.19.4)
130
+ thor (~> 1.0)
120
131
  tilt (~> 2.0)
121
- yard (~> 0.9)
122
- thor (0.20.3)
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.6)
127
- unicode-display_width (1.6.0)
128
- yard (0.9.20)
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.3)
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
- [![Actions Status](https://github.com/ianks/attr-gather/workflows/.github/workflows/ruby.yml/badge.svg)](https://github.com/ianks/attr-gather/actions)
3
+ ![Actions Status](https://github.com/ianks/attr-gather/workflows/Build%20+%20Test%20+%20Lint/badge.svg)
4
4
 
5
- A gem for creating simple workflows to enhance entities with extra attributes.
6
- At a high level, `attr-gather` provides a process to sync attributes from many
7
- sources (third party APIs, legacy databases, etc).
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
 
@@ -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
@@ -28,7 +28,7 @@ class MyContainer
28
28
  end
29
29
 
30
30
  register :email_info do |user:, **_attrs|
31
- res = HTTP.timeout(3).get("https://api.trumail.io/v2/lookups/json?email=#{user[:email]}")
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
- unvalidated = res.result.value!
30
-
31
- return unvalidated if filter.nil?
33
+ return res if filter.nil?
32
34
 
33
- filter.call(unvalidated).value
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
- result = execution_results.reduce(input.dup) do |memo, res|
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
- items = reverse? ? execution_results.reverse_each : execution_results
23
+ execution_results = execution_results.reverse_each if reverse?
24
24
 
25
- result = items.reduce(input.dup) do |memo, res|
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
@@ -14,8 +14,8 @@ module Attr
14
14
  # @param dry_contract [Dry::Contract]
15
15
  def initialize(dry_contract)
16
16
  validate_dry_contract!(dry_contract)
17
-
18
17
  @dry_contract = dry_contract
18
+ super()
19
19
  end
20
20
 
21
21
  def call(input)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Attr
4
4
  module Gather
5
- VERSION = '1.1.0'
5
+ VERSION = '1.3.0'
6
6
  end
7
7
  end
@@ -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
- final_results = []
27
+ task_promises = {}
31
28
 
32
- each_task_batch.reduce(input.dup) do |aggregated_input, batch|
33
- executor_results = execute_batch(aggregated_input, batch)
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
- aggregator.call(input.dup, final_results.flatten(1))
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 execute_batch(aggregated_input, batch)
58
- executor = AsyncTaskExecutor.new(batch, container: container)
59
- executor.call(aggregated_input)
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
- lines = row.map { |item| item.map(&:name).join(' -> ') + ';' }
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
- # extend Attr::Gather::Workflow
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
- task = Task.new(name: task_name, **opts)
40
- yield task
41
- tasks << task
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
- # extend Attr::Gather::Workflow
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
- # extend Attr::Gather::Workflow
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
- @aggregator = Aggregators.default
93
- return @aggregator
94
- end
95
-
96
- @aggregator = Aggregators.resolve(agg, filter: filter, **opts) if agg
97
- @aggregator
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
- # extend Attr::Gather::Workflow
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 = Undefined, *args)
134
- if filt == Undefined && !defined?(@filter)
135
- @filter = Filters.default
136
- elsif filt != Undefined
137
- @filter = Filters.resolve(filt, *args)
138
- end
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
- # extend Attr::Gather::Workflow
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
- @filter = Filters.resolve(:contract, contract)
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
- attr_accessor :depends_on, :name
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.name)
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 <<(task)
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.map(&:name).include?(t) }
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.1.0
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-10-21 00:00:00.000000000 Z
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: '0'
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