attr-gather 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee9b30fd44bd8931964b53b904815224fa6a5a592ac1f22d19605a982a326d42
4
- data.tar.gz: 7954a71a61dc89266358909ce189312b3b3164e4092d62ae4fae973712efca9d
3
+ metadata.gz: 56fa2344f15f389295db52f8201720d91bae7114d5d68472208d5509fe40d35d
4
+ data.tar.gz: cce04b8b3ebe30523f0b4b5528f2531d5607e1905e6869f21076ac7eeac39e34
5
5
  SHA512:
6
- metadata.gz: 040b490aba781f8f851b617a365d2aea061f9d2afb189bcdf4e0e086effebc2135109a648b0f116d4be7000cbe4d3e10e369fadcb38d9e2910114c36957cce68
7
- data.tar.gz: 59a93d0d6e16f9b2433822c4faf7ef4cda25f3e9e37da8a5fae816e313e1d6e0b86906a1d652b3c64125eae888607c9117ade38fc17ff11b84c361add21f4c62
6
+ metadata.gz: dad243cf700400e4bab5abb9134704870885c711a6a4c4eedf185860af66485493f16d2a2fda99b103e1ab7b5238c244e16a73a7b1264b9f59cee6f42bbdf2b7
7
+ data.tar.gz: 46441ed69c96018c33c4676e3e48b3b06bf39de8aaa2f16c20fdda026375bd66330064c11ac8ea4f7499b0038c98309a3f958ea532ab6da1476dad2e5e070de1
data/.github/mergify.yml CHANGED
@@ -13,7 +13,10 @@ pull_request_rules:
13
13
  conditions:
14
14
  - author~=^dependabot(|-preview)\[bot\]$
15
15
  - "#approved-reviews-by>=1"
16
- - "check-success=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
@@ -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
data/.rubocop.yml CHANGED
@@ -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.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- attr-gather (1.2.0)
4
+ attr-gather (1.2.1)
5
5
  dry-container (~> 0.7)
6
6
 
7
7
  GEM
@@ -14,7 +14,7 @@ GEM
14
14
  benchmark (0.1.0)
15
15
  coderay (1.1.3)
16
16
  concurrent-ruby (1.1.7)
17
- diff-lcs (1.3)
17
+ diff-lcs (1.4.4)
18
18
  domain_name (0.5.20190701)
19
19
  unf (>= 0.0.5, < 1.0.0)
20
20
  dry-configurable (0.11.6)
@@ -76,7 +76,7 @@ GEM
76
76
  mini_portile2 (2.4.0)
77
77
  nokogiri (1.10.10)
78
78
  mini_portile2 (~> 2.4.0)
79
- parallel (1.19.2)
79
+ parallel (1.20.1)
80
80
  parser (2.7.2.0)
81
81
  ast (~> 2.4.1)
82
82
  pry (0.13.1)
@@ -85,29 +85,35 @@ GEM
85
85
  public_suffix (4.0.6)
86
86
  rainbow (3.0.0)
87
87
  rake (13.0.1)
88
+ regexp_parser (2.0.0)
88
89
  reverse_markdown (2.0.0)
89
90
  nokogiri
90
- rspec (3.9.0)
91
- rspec-core (~> 3.9.0)
92
- rspec-expectations (~> 3.9.0)
93
- rspec-mocks (~> 3.9.0)
94
- rspec-core (3.9.0)
95
- rspec-support (~> 3.9.0)
96
- 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)
97
99
  diff-lcs (>= 1.2.0, < 2.0)
98
- rspec-support (~> 3.9.0)
99
- rspec-mocks (3.9.0)
100
+ rspec-support (~> 3.10.0)
101
+ rspec-mocks (3.10.0)
100
102
  diff-lcs (>= 1.2.0, < 2.0)
101
- rspec-support (~> 3.9.0)
102
- rspec-support (3.9.0)
103
- rubocop (0.75.1)
104
- jaro_winkler (~> 1.5.1)
103
+ rspec-support (~> 3.10.0)
104
+ rspec-support (3.10.0)
105
+ rubocop (0.93.1)
105
106
  parallel (~> 1.10)
106
- parser (>= 2.6)
107
+ parser (>= 2.7.1.5)
107
108
  rainbow (>= 2.2.2, < 4.0)
109
+ regexp_parser (>= 1.8)
110
+ rexml
111
+ rubocop-ast (>= 0.6.0)
108
112
  ruby-progressbar (~> 1.7)
109
- unicode-display_width (>= 1.4.0, < 1.7)
110
- 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)
111
117
  rubocop (>= 0.71.0)
112
118
  ruby-progressbar (1.10.1)
113
119
  solargraph (0.39.17)
@@ -129,7 +135,7 @@ GEM
129
135
  unf (0.1.4)
130
136
  unf_ext
131
137
  unf_ext (0.0.7.7)
132
- unicode-display_width (1.6.1)
138
+ unicode-display_width (1.7.0)
133
139
  yard (0.9.25)
134
140
 
135
141
  PLATFORMS
data/README.md CHANGED
@@ -1,10 +1,148 @@
1
1
  # attr-gather
2
2
 
3
- [![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
+ [![Maintainability](https://api.codeclimate.com/v1/badges/b825a3bc37ad6a76e005/maintainability)](https://codeclimate.com/github/ianks/attr-gather/maintainability)
4
5
 
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).
6
+ A gem for creating workflows that "enhance" entities with extra attributes. At a high level, [attr-gather](https://github.com/ianks/attr-gather) provides a process to fetch information from many data sources (such as third party APIs, legacy databases, etc.) in a fully parallelized fashion.
7
+
8
+ ## Usage
9
+
10
+ ### Defining your workflow
11
+
12
+ ```ruby
13
+ # define a workflow
14
+ class EnhanceProfile
15
+ include Attr::Gather::Workflow
16
+
17
+ # contains all the task implementations
18
+ container TasksContainer
19
+
20
+ # filter out invalid data using a Dry::Validation::Contract
21
+ # anything that doesn't match this schema will be filtered out
22
+ filter_with_contract do
23
+ params do
24
+ required(:user_id).filled(:integer)
25
+
26
+ optional(:user).hash do
27
+ optional(:name).filled(:string)
28
+ optional(:email).filled(:string)
29
+ optional(:gravatar).filled(:string)
30
+ optional(:email_info).hash do
31
+ optional(:deliverable).filled(:bool?)
32
+ optional(:free).filled(:bool?)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # each task returns a hash of data that will be merged into the result
39
+ task :fetch_post do |t|
40
+ t.depends_on = []
41
+ end
42
+
43
+ # will run in parallel
44
+ task :fetch_user do |t|
45
+ t.depends_on = [:fetch_post]
46
+ end
47
+
48
+ # will run in parallel
49
+ task :fetch_email_info do |t|
50
+ t.depends_on = [:fetch_user]
51
+ end
52
+ end
53
+ ```
54
+
55
+ ### Defining some tasks
56
+
57
+ ```ruby
58
+ class PostFetcher
59
+ def call(attrs)
60
+ res = HTTP.get("https://jsonplaceholder.typicode.com/posts/#{attrs[:id]}")
61
+ post = JSON.parse(res.to_s, symbolize_names: true)
62
+
63
+ { title: post[:title], user_id: post[:userId], body: post[:body] }
64
+ end
65
+ end
66
+ ```
67
+
68
+ ```ruby
69
+ class UserFetcher
70
+ # will have access to the PostFetcher attributes here
71
+ def call(attrs)
72
+ res = HTTP.get("https://jsonplaceholder.typicode.com/users/#{attrs[:user_id]}")
73
+ user = JSON.parse(res.to_s, symbolize_names: true)
74
+
75
+ { user: { name: user[:name], email: user[:email] } }
76
+ end
77
+ end
78
+ ```
79
+
80
+ ```ruby
81
+ class EmailInfoFetcher
82
+ # will have access to the PostFetcher attributes here
83
+ def call(user:)
84
+ res = HTTP.timeout(3).get("https://api.trumail.io/v2/lookups/json?email=#{user[:email]}")
85
+ info = JSON.parse(res.to_s, symbolize_names: true)
86
+
87
+ # will deep merge with the final result
88
+ { user: { email_info: { deliverable: info[:deliverable], free: info[:free] } } }
89
+ end
90
+ end
91
+ ```
92
+
93
+ ### Registering your tasks
94
+
95
+ ```ruby
96
+ class MyContainer
97
+ extend Dry::Container::Mixin
98
+
99
+ register :fetch_post, PostFetcher
100
+ register :fetch_user, UserFetcher
101
+ register :fetch_email_info, EmailInfoFetcher
102
+ end
103
+ ```
104
+
105
+ ### Run it!
106
+
107
+ ```ruby
108
+ enhancer = EnhanceUserProfile.new
109
+ enhancer.call(id: 12).value!
110
+ ```
111
+
112
+ And this is the result...
113
+
114
+ ```ruby
115
+ {
116
+ :id => 12,
117
+ :user_id => 2,
118
+ :user => {
119
+ :email => "Shanna@melissa.tv",
120
+ :name => "Ervin Howell",
121
+ :email_info => { :deliverable => true, :free => true },
122
+ :gravatar => "https://www.gravatar.com/avatar/241af7d19a0a7438794aef21e4e19b79"
123
+ }
124
+ }
125
+ ```
126
+
127
+ You can even preview it as an SVG!
128
+
129
+ ```ruby
130
+ enhancer.to_dot(preview: true) # requires graphviz (brew install graphviz)
131
+ ```
132
+
133
+ ## Features
134
+
135
+ - Offers DSL for defining workflows and merging the results from each task
136
+ - Execution engine optimally parallelizes the execution of the workflow dependency graph using [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) Promises
137
+ - Very easy to unit test
138
+ - Ability to filter out bad/junky data using [dry-validation](https://dry-rb.org/gems/dry-validation) contracts
139
+
140
+ ## What are the main difference between this Ruby project and similar ones?
141
+
142
+ - Operates on a single entity rather than a list, so easily adoptable in existing systems
143
+ - Focuses on the "fetching" and filtering of data solely, and not transformation or storage
144
+ - Focuses on having a clean PORO interface to make testing simple
145
+ - Provides a declarative interface for merging results from many sources (APIs, legacy databases, etc.) which allows for prioritization
8
146
 
9
147
  ## Links
10
148
 
data/attr-gather.gemspec CHANGED
@@ -15,6 +15,8 @@ Gem::Specification.new do |spec|
15
15
  spec.homepage = 'https://github.com/ianks/attr-gather'
16
16
  spec.license = 'MIT'
17
17
 
18
+ spec.required_ruby_version = '>= 2.4'
19
+
18
20
  # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
19
21
 
20
22
  spec.metadata['homepage_uri'] = spec.homepage
@@ -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] } } }
@@ -12,28 +12,25 @@ module Attr
12
12
  # Initialize a new DeepMerge aggregator
13
13
  #
14
14
  # @param reverse [Boolean] deep merge results in reverse order
15
+ # @param merge_input [Boolean] merge the result with the initial input
15
16
  #
16
17
  # @api private
17
- def initialize(reverse: false, **)
18
+ def initialize(reverse: false, merge_input: true, **)
18
19
  @reverse = reverse
20
+ @merge_input = merge_input
19
21
  super
20
22
  end
21
23
 
22
24
  def call(input, execution_results)
23
25
  execution_results = execution_results.reverse_each if reverse?
24
- initial = unwrap_initial_input(input)
25
26
 
26
- execution_results.reduce(initial) do |memo, res|
27
+ execution_results.reduce(@merge_input ? input : EMPTY_HASH) do |memo, res|
27
28
  deep_merge(memo, unwrap_result(res))
28
29
  end
29
30
  end
30
31
 
31
32
  private
32
33
 
33
- def unwrap_initial_input(input)
34
- input
35
- end
36
-
37
34
  def reverse?
38
35
  @reverse
39
36
  end
@@ -12,17 +12,19 @@ module Attr
12
12
  # Initialize a new DeepMerge aggregator
13
13
  #
14
14
  # @param reverse [Boolean] merge results in reverse order
15
+ # @param merge_input [Boolean] merge the result with the initial input
15
16
  #
16
17
  # @api private
17
- def initialize(reverse: false, **)
18
+ def initialize(reverse: false, merge_input: true, **)
18
19
  @reverse = reverse
20
+ @merge_input = merge_input
19
21
  super
20
22
  end
21
23
 
22
24
  def call(input, execution_results)
23
25
  execution_results = execution_results.reverse_each if reverse?
24
26
 
25
- execution_results.reduce(input) do |memo, res|
27
+ execution_results.reduce(@merge_input ? input : EMPTY_HASH) do |memo, res|
26
28
  memo.merge(unwrap_result(res))
27
29
  end
28
30
  end
@@ -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.2.0'
5
+ VERSION = '1.2.1'
6
6
  end
7
7
  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
- 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
  #
@@ -42,6 +42,56 @@ module Attr
42
42
  self
43
43
  end
44
44
 
45
+ # Defines a task with name and options
46
+ #
47
+ # @param task_name [Symbol] the name of the task
48
+ #
49
+ # @example
50
+ # class EnhanceUserProfile
51
+ # include Attr::Gather::Workflow
52
+ #
53
+ # # ...
54
+ #
55
+ # fetch :user_info do |t|
56
+ # t.depends_on = [:fetch_gravatar_info]
57
+ # end
58
+ # end
59
+ #
60
+ # Calling `fetch` will yield a task object which you can configure like
61
+ # a PORO. Tasks will be registered for execution in the workflow.
62
+ #
63
+ # @yield [Attr::Gather::Workflow::Task] A task to configure
64
+ #
65
+ # @api public
66
+ def fetch(task_name, opts = EMPTY_HASH)
67
+ task(task_name, opts)
68
+ end
69
+
70
+ # Defines a task with name and options
71
+ #
72
+ # @param task_name [Symbol] the name of the task
73
+ #
74
+ # @example
75
+ # class EnhanceUserProfile
76
+ # include Attr::Gather::Workflow
77
+ #
78
+ # # ...
79
+ #
80
+ # step :fetch_user_info do |t|
81
+ # t.depends_on = [:email_info]
82
+ # end
83
+ # end
84
+ #
85
+ # Calling `step` will yield a task object which you can configure like
86
+ # a PORO. Tasks will be registered for execution in the workflow.
87
+ #
88
+ # @yield [Attr::Gather::Workflow::Task] A task to configure
89
+ #
90
+ # @api public
91
+ def step(task_name, opts = EMPTY_HASH)
92
+ task(task_name, opts)
93
+ end
94
+
45
95
  # Defines a container for task dependencies
46
96
  #
47
97
  # Using a container makes it easy to re-use workflows with different
@@ -55,7 +105,7 @@ module Attr
55
105
  # end
56
106
  #
57
107
  # class EnhanceUserProfile
58
- # 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
@@ -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.
@@ -151,7 +201,7 @@ module Attr
151
201
  # @example
152
202
  #
153
203
  # class EnhanceUserProfile
154
- # extend Attr::Gather::Workflow
204
+ # include Attr::Gather::Workflow
155
205
  #
156
206
  # # Any of the key/value pairs that had validation errors will be
157
207
  # # filtered from the output.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attr-gather
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Ker-Seymer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-22 00:00:00.000000000 Z
11
+ date: 2021-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -135,7 +135,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: '2.4'
139
139
  required_rubygems_version: !ruby/object:Gem::Requirement
140
140
  requirements:
141
141
  - - ">="