attr-gather 1.1.0 → 1.3.0

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 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