attr-gather 1.2.0 → 1.2.1

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: 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
  - - ">="