rails-patterns 0.3.0 → 0.7.3

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
- SHA1:
3
- metadata.gz: 2318f3f2a2753ab5159fa535d0245110eca7133c
4
- data.tar.gz: 6031c700f903e0a62ef3a9951f83932d590a887c
2
+ SHA256:
3
+ metadata.gz: ffa6ee8fff6415c7271eb4a38172148881cdd8866d0279e59abffd452b54f591
4
+ data.tar.gz: a8e077bf276d9a6a3a05f345d09085561ccb5f06996b7c7a8a01f4082cf329b5
5
5
  SHA512:
6
- metadata.gz: 4eabca7c85d89675918642a3f07c72effce0fccb4d709cc4d13c986cb549b9216dc7913cf2303ca464efa69c157b168f3d71301dd0422af13c738fce639e47e4
7
- data.tar.gz: b3d706d159d0abedab70f3aadb818c1b977463aa37e683f8a5bd2b3d139a187e3ac72d4b18b98def990ff739ce5d5b224b9de2f03c27e5585283f4213d4479df
6
+ metadata.gz: d4e616146039004e697d7a2b3fc3ef89eaaaaf11779af4744a605d2102b967b8b4b9856147a1b111db5c801fde833ca19c0c5ba067a246977a14c14d7f9c2f54
7
+ data.tar.gz: 1e883e3cbfc21fc4e7135acc9b23dc4db7e05b2a4671cfe6d618f04b5df4d672c7ac8f76487ae34cce503fd2fdc51e2a0952dffbc66964943c85b8136a0dd509
@@ -0,0 +1,33 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ ruby: [ '2.5', '2.6', '2.7' ]
23
+ name: RSpec for Ruby version ${{ matrix.ruby }}
24
+ steps:
25
+ - uses: actions/checkout@v2
26
+ - uses: supercharge/redis-github-action@1.1.0
27
+ - name: Set up Ruby
28
+ uses: actions/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{ matrix.ruby }}
31
+ - run: gem install bundler
32
+ - run: bundle install
33
+ - run: bundle exec rspec
data/Gemfile CHANGED
@@ -1,17 +1,21 @@
1
1
  source "https://rubygems.org"
2
- # Add dependencies required to use your gem here.
3
- # Example:
4
- gem "activerecord", ">= 4.2.6"
2
+
3
+ gem "activerecord", ">= 4.2.6"
4
+ gem "actionpack", ">= 4.2.6"
5
+ gem "virtus"
6
+ gem "ruby2_keywords"
5
7
 
6
8
  # Add dependencies to develop your gem here.
7
9
  # Include everything needed to run rake, tests, features, etc.
8
10
 
9
11
  group :development do
10
12
  gem "rspec"
11
- gem "bundler", "~> 1.0"
13
+ gem "bundler", "~> 2.0"
12
14
  gem "juwelier", "~> 2.1.0"
13
15
  end
14
16
 
15
17
  group "test" do
16
18
  gem "pry-rails"
19
+ gem "rspec_junit_formatter"
20
+ gem "redis"
17
21
  end
@@ -1,38 +1,62 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- activemodel (5.0.2)
5
- activesupport (= 5.0.2)
6
- activerecord (5.0.2)
7
- activemodel (= 5.0.2)
8
- activesupport (= 5.0.2)
9
- arel (~> 7.0)
10
- activesupport (5.0.2)
4
+ actionpack (6.0.3.1)
5
+ actionview (= 6.0.3.1)
6
+ activesupport (= 6.0.3.1)
7
+ rack (~> 2.0, >= 2.0.8)
8
+ rack-test (>= 0.6.3)
9
+ rails-dom-testing (~> 2.0)
10
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
11
+ actionview (6.0.3.1)
12
+ activesupport (= 6.0.3.1)
13
+ builder (~> 3.1)
14
+ erubi (~> 1.4)
15
+ rails-dom-testing (~> 2.0)
16
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
17
+ activemodel (6.0.3.1)
18
+ activesupport (= 6.0.3.1)
19
+ activerecord (6.0.3.1)
20
+ activemodel (= 6.0.3.1)
21
+ activesupport (= 6.0.3.1)
22
+ activesupport (6.0.3.1)
11
23
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
- i18n (~> 0.7)
24
+ i18n (>= 0.7, < 2)
13
25
  minitest (~> 5.1)
14
26
  tzinfo (~> 1.1)
15
- addressable (2.4.0)
16
- arel (7.1.4)
17
- builder (3.2.3)
18
- coderay (1.1.1)
19
- concurrent-ruby (1.0.5)
27
+ zeitwerk (~> 2.2, >= 2.2.2)
28
+ addressable (2.7.0)
29
+ public_suffix (>= 2.0.2, < 5.0)
30
+ axiom-types (0.1.1)
31
+ descendants_tracker (~> 0.0.4)
32
+ ice_nine (~> 0.11.0)
33
+ thread_safe (~> 0.3, >= 0.3.1)
34
+ builder (3.2.4)
35
+ coderay (1.1.2)
36
+ coercible (1.0.0)
37
+ descendants_tracker (~> 0.0.1)
38
+ concurrent-ruby (1.1.6)
39
+ crass (1.0.6)
20
40
  descendants_tracker (0.0.4)
21
41
  thread_safe (~> 0.3, >= 0.3.1)
22
42
  diff-lcs (1.3)
23
- faraday (0.9.2)
43
+ equalizer (0.0.11)
44
+ erubi (1.9.0)
45
+ faraday (0.17.3)
24
46
  multipart-post (>= 1.2, < 3)
25
- git (1.3.0)
26
- github_api (0.16.0)
27
- addressable (~> 2.4.0)
47
+ git (1.7.0)
48
+ rchardet (~> 1.8)
49
+ github_api (0.18.2)
50
+ addressable (~> 2.4)
28
51
  descendants_tracker (~> 0.0.4)
29
- faraday (~> 0.8, < 0.10)
30
- hashie (>= 3.4)
31
- mime-types (>= 1.16, < 3.0)
52
+ faraday (~> 0.8)
53
+ hashie (~> 3.5, >= 3.5.2)
32
54
  oauth2 (~> 1.0)
33
- hashie (3.5.5)
34
- highline (1.7.8)
35
- i18n (0.8.1)
55
+ hashie (3.6.0)
56
+ highline (2.0.3)
57
+ i18n (1.8.2)
58
+ concurrent-ruby (~> 1.0)
59
+ ice_nine (0.11.2)
36
60
  juwelier (2.1.3)
37
61
  builder
38
62
  bundler (>= 1.13)
@@ -43,59 +67,83 @@ GEM
43
67
  rake
44
68
  rdoc
45
69
  semver
46
- jwt (1.5.6)
47
- method_source (0.8.2)
48
- mime-types (2.99.3)
49
- mini_portile2 (2.1.0)
50
- minitest (5.10.1)
51
- multi_json (1.12.1)
70
+ jwt (2.2.1)
71
+ loofah (2.5.0)
72
+ crass (~> 1.0.2)
73
+ nokogiri (>= 1.5.9)
74
+ method_source (0.9.0)
75
+ mini_portile2 (2.4.0)
76
+ minitest (5.14.1)
77
+ multi_json (1.14.1)
52
78
  multi_xml (0.6.0)
53
- multipart-post (2.0.0)
54
- nokogiri (1.7.1)
55
- mini_portile2 (~> 2.1.0)
56
- oauth2 (1.3.1)
57
- faraday (>= 0.8, < 0.12)
58
- jwt (~> 1.0)
79
+ multipart-post (2.1.1)
80
+ nokogiri (1.10.9)
81
+ mini_portile2 (~> 2.4.0)
82
+ oauth2 (1.4.4)
83
+ faraday (>= 0.8, < 2.0)
84
+ jwt (>= 1.0, < 3.0)
59
85
  multi_json (~> 1.3)
60
86
  multi_xml (~> 0.5)
61
87
  rack (>= 1.2, < 3)
62
- pry (0.10.4)
88
+ pry (0.11.3)
63
89
  coderay (~> 1.1.0)
64
- method_source (~> 0.8.1)
65
- slop (~> 3.4)
90
+ method_source (~> 0.9.0)
66
91
  pry-rails (0.3.6)
67
92
  pry (>= 0.10.4)
68
- rack (2.0.1)
69
- rake (12.0.0)
70
- rdoc (5.1.0)
71
- rspec (3.5.0)
72
- rspec-core (~> 3.5.0)
73
- rspec-expectations (~> 3.5.0)
74
- rspec-mocks (~> 3.5.0)
75
- rspec-core (3.5.4)
76
- rspec-support (~> 3.5.0)
77
- rspec-expectations (3.5.0)
93
+ public_suffix (4.0.5)
94
+ rack (2.2.2)
95
+ rack-test (1.1.0)
96
+ rack (>= 1.0, < 3)
97
+ rails-dom-testing (2.0.3)
98
+ activesupport (>= 4.2.0)
99
+ nokogiri (>= 1.6)
100
+ rails-html-sanitizer (1.3.0)
101
+ loofah (~> 2.3)
102
+ rake (13.0.1)
103
+ rchardet (1.8.0)
104
+ rdoc (6.2.1)
105
+ redis (4.1.4)
106
+ rspec (3.7.0)
107
+ rspec-core (~> 3.7.0)
108
+ rspec-expectations (~> 3.7.0)
109
+ rspec-mocks (~> 3.7.0)
110
+ rspec-core (3.7.1)
111
+ rspec-support (~> 3.7.0)
112
+ rspec-expectations (3.7.0)
78
113
  diff-lcs (>= 1.2.0, < 2.0)
79
- rspec-support (~> 3.5.0)
80
- rspec-mocks (3.5.0)
114
+ rspec-support (~> 3.7.0)
115
+ rspec-mocks (3.7.0)
81
116
  diff-lcs (>= 1.2.0, < 2.0)
82
- rspec-support (~> 3.5.0)
83
- rspec-support (3.5.0)
117
+ rspec-support (~> 3.7.0)
118
+ rspec-support (3.7.1)
119
+ rspec_junit_formatter (0.3.0)
120
+ rspec-core (>= 2, < 4, != 2.12.0)
121
+ ruby2_keywords (0.0.2)
84
122
  semver (1.0.1)
85
- slop (3.6.0)
86
123
  thread_safe (0.3.6)
87
- tzinfo (1.2.3)
124
+ tzinfo (1.2.7)
88
125
  thread_safe (~> 0.1)
126
+ virtus (1.0.5)
127
+ axiom-types (~> 0.1)
128
+ coercible (~> 1.0)
129
+ descendants_tracker (~> 0.0, >= 0.0.3)
130
+ equalizer (~> 0.0, >= 0.0.9)
131
+ zeitwerk (2.3.0)
89
132
 
90
133
  PLATFORMS
91
134
  ruby
92
135
 
93
136
  DEPENDENCIES
137
+ actionpack (>= 4.2.6)
94
138
  activerecord (>= 4.2.6)
95
- bundler (~> 1.0)
139
+ bundler (~> 2.0)
96
140
  juwelier (~> 2.1.0)
97
141
  pry-rails
142
+ redis
98
143
  rspec
144
+ rspec_junit_formatter
145
+ ruby2_keywords
146
+ virtus
99
147
 
100
148
  BUNDLED WITH
101
- 1.14.6
149
+ 2.1.4
data/README.md CHANGED
@@ -1,6 +1,14 @@
1
+ ![](https://github.com/Selleo/pattern/workflows/Ruby/badge.svg)
2
+
1
3
  # Pattern
2
4
 
3
- A collection of lightweight, standardized, rails-oriented patterns.
5
+ A collection of lightweight, standardized, rails-oriented patterns used by [RubyOnRails Developers @ Selleo](https://selleo.com/ruby-on-rails)
6
+
7
+ - [Query - complex querying on active record relation](#query)
8
+ - [Service - useful for handling processes involving multiple steps](#service)
9
+ - [Collection - when in need to add a method that relates to the collection as whole](#collection)
10
+ - [Form - when you need a place for callbacks, want to replace strong parameters or handle virtual/composite resources](#form)
11
+ - [Calculation - when you need a place for calculating a simple value (numeric, array, hash) and/or cache it](#calculation)
4
12
 
5
13
  ## Installation
6
14
 
@@ -22,6 +30,7 @@ One should consider using query objects pattern when in need to perform complex
22
30
  Usually one should avoid using scopes for such purpose.
23
31
  As a rule of thumb, if scope interacts with more than one column and/or joins in other tables, it should be moved to query object.
24
32
  Also whenever a chain of scopes is to be used, one should consider using query object too.
33
+ Some more information on using query objects can be found in [this article](https://medium.com/@blazejkosmowski/essential-rubyonrails-patterns-part-2-query-objects-4b253f4f4539).
25
34
 
26
35
  ### Assumptions and rules
27
36
 
@@ -68,7 +77,7 @@ RecentlyActivatedUsersQuery.call(date_range: Date.today.beginning_of_day..Date.t
68
77
  RecentlyActivatedUsersQuery.call(User.without_test_users, date_range: Date.today.beginning_of_day..Date.today.end_of_day)
69
78
 
70
79
  class User < ApplicationRecord
71
- scope :recenty_activated, RecentlyActivatedUsersQuery
80
+ scope :recently_activated, RecentlyActivatedUsersQuery
72
81
  end
73
82
  ```
74
83
 
@@ -77,7 +86,7 @@ end
77
86
  ### When to use it
78
87
 
79
88
  Service objects are commonly used to mitigate problems with model callbacks that interact with external classes ([read more...](http://samuelmullen.com/2013/05/the-problem-with-rails-callbacks/)).
80
- Service objects are also useful for handling processes involving multiple steps. E.g. a controller that performs more than one operation on its subject (usually a model instance) is a possible candidate for Extract ServiceObject (or Extract FormObject) refactoring.
89
+ Service objects are also useful for handling processes involving multiple steps. E.g. a controller that performs more than one operation on its subject (usually a model instance) is a possible candidate for Extract ServiceObject (or Extract FormObject) refactoring. In many cases service object can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object). Some more information on using services can be found in [this article](https://medium.com/selleo/essential-rubyonrails-patterns-part-1-service-objects-1af9f9573ca1).
81
90
 
82
91
  ### Assumptions and rules
83
92
 
@@ -88,6 +97,10 @@ Service objects are also useful for handling processes involving multiple steps.
88
97
  * It is recommended for `#call` method to be the only public method of service object (besides state readers)
89
98
  * It is recommended to name service object classes after commands (e.g. `ActivateUser` instead of `UserActivation`)
90
99
 
100
+ ### Other
101
+
102
+ A bit higher level of abstraction is provided by [business_process gem](https://github.com/Selleo/business_process).
103
+
91
104
  ### Examples
92
105
 
93
106
  #### Declaration
@@ -157,7 +170,7 @@ class CustomerEventsByTypeCollection < Patterns::Collection
157
170
  subject.
158
171
  events.
159
172
  group_by(&:type).
160
- transform_values{ |event| event.public_send(options.fetch(:label_method, "description")) }
173
+ transform_values{ |events| events.map{ |e| e.public_send(options.fetch(:label_method, "description")) }}
161
174
  end
162
175
  end
163
176
  ```
@@ -166,8 +179,172 @@ end
166
179
 
167
180
  ```ruby
168
181
  ColorsCollection.new
169
- CustomerEventsCollection.for(customer)
170
- CustomerEventsCollection.for(customer, label_method: "name")
182
+ CustomerEventsByTypeCollection.for(customer)
183
+ CustomerEventsByTypeCollection.for(customer, label_method: "name")
184
+ ```
185
+
186
+ ## Form
187
+
188
+ ### When to use it
189
+
190
+ Form objects, just like service objects, are commonly used to mitigate problems with model callbacks that interact with external classes ([read more...](http://samuelmullen.com/2013/05/the-problem-with-rails-callbacks/)).
191
+ Form objects can also be used as replacement for `ActionController::StrongParameters` strategy, as all writable attributes are re-defined within each form.
192
+ Finally form objects can be used as wrappers for virtual (with no model representation) or composite (saving multiple models at once) resources.
193
+ In the latter case this may act as replacement for `ActiveRecord::NestedAttributes`.
194
+ In some cases FormObject can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object). Some more information on using form objects can be found in [this article](https://medium.com/selleo/essential-rubyonrails-patterns-form-objects-b199aada6ec9).
195
+
196
+ ### Assumptions and rules
197
+
198
+ * Forms include `ActiveModel::Validations` to support validation.
199
+ * Forms include `Virtus.model` to support `attribute` static method with all [corresponding capabilities](https://github.com/solnic/virtus).
200
+ * Forms can be initialized using `.new`.
201
+ * Forms accept optional resource object as first constructor argument.
202
+ * Forms accept optional attributes hash as latter constructor argument.
203
+ * Forms have to implement `#persist` method that returns falsey (if failed) or truthy (if succeeded) value.
204
+ * Forms provide access to first constructor argument using `#resource`.
205
+ * Forms are saved using their `#save` or `#save!` methods.
206
+ * Forms will attempt to pre-populate their fields using `resource#attributes` and public getters for `resource`
207
+ * Form's fields are populated with passed-in attributes hash reverse-merged with pre-populated attributes if possible.
208
+ * Forms provide `#as` builder method that populates internal `@form_owner` variable (can be used to store current user).
209
+ * Forms allow defining/overriding their `#param_key` method result by using `.param_key` static method. This defaults to `#resource#model_name#param_key`.
210
+ * Forms delegate `#persisted?` method to `#resource` if possible.
211
+ * Forms do handle `ActionController::Parameters` as attributes hash (using `to_unsafe_h`)
212
+ * It is recommended to wrap `#persist` method in transaction if possible and if multiple model are affected.
213
+
214
+ ### Examples
215
+
216
+ #### Declaration
217
+
218
+ ```ruby
219
+ class UserForm < Patterns::Form
220
+ param_key "person"
221
+
222
+ attribute :first_name, String
223
+ attribute :last_name, String
224
+ attribute :age, Integer
225
+ attribute :full_address, String
226
+ attribute :skip_notification, Boolean
227
+
228
+ validate :first_name, :last_name, presence: true
229
+
230
+ private
231
+
232
+ def persist
233
+ update_user and
234
+ update_address and
235
+ deliver_notification
236
+ end
237
+
238
+ def update_user
239
+ resource.update_attributes(attributes.except(:full_address, :skip_notification))
240
+ end
241
+
242
+ def update_address
243
+ resource.address.update_attributes(full_address: full_address)
244
+ end
245
+
246
+ def deliver_notification
247
+ skip_notification || UserNotifier.user_update_notification(user, form_owner).deliver
248
+ end
249
+ end
250
+
251
+ class ReportConfigurationForm < Patterns::Form
252
+ param_key "report"
253
+
254
+ attribute :include_extra_data, Boolean
255
+ attribute :dump_as_csv, Boolean
256
+ attribute :comma_separated_column_names, String
257
+ attribute :date_start, Date
258
+ attribute :date_end, Date
259
+
260
+ private
261
+
262
+ def persist
263
+ SendReport.call(attributes)
264
+ end
265
+ end
266
+ ```
267
+
268
+ #### Usage
269
+
270
+ ```ruby
271
+ form = UserForm.new(User.find(1), params[:person])
272
+ form.save
273
+
274
+ form = UserForm.new(User.new, params[:person]).as(current_user)
275
+ form.save!
276
+
277
+ ReportConfigurationForm.new
278
+ ReportConfigurationForm.new({ include_extra_data: true, dump_as_csv: true })
279
+ ```
280
+
281
+ ## Calculation
282
+
283
+ ### When to use it
284
+
285
+ Calculation objects provide a place to calculate simple values (i.e. numeric, arrays, hashes), especially when calculations require interacting with multiple classes, and thus do not fit into any particular one.
286
+ Calculation objects also provide simple abstraction for caching their results.
287
+
288
+ ### Assumptions and rules
289
+
290
+ * Calculations have to implement `#result` method that returns any value (result of calculation).
291
+ * Calculations do provide `.set_cache_expiry_every` method, that allows defining caching period.
292
+ * When `.set_cache_expiry_every` is not used, result is not being cached.
293
+ * Calculations return result by calling any of following methods: `.calculate`, `.result_for` or `.result`.
294
+ * First argument passed to calculation is accessible by `#subject` private method.
295
+ * Arguments hash passed to calculation is accessible by `#options` private method.
296
+ * Caching takes into account arguments passed when building cache key.
297
+ * To build cache key, `#cache_key` of each argument value is used if possible.
298
+ * By default `Rails.cache` is used as cache store.
299
+
300
+ ### Examples
301
+
302
+ #### Declaration
303
+
304
+ ```ruby
305
+ class AverageHotelDailyRevenue < Patterns::Calculation
306
+ set_cache_expiry_every 1.day
307
+
308
+ private
309
+
310
+ def result
311
+ reservations.sum(:price) / days_in_year
312
+ end
313
+
314
+ def reservations
315
+ Reservation.where(
316
+ date: (beginning_of_year..end_of_year),
317
+ hotel_id: subject.id
318
+ )
319
+ end
320
+
321
+ def days_in_year
322
+ end_of_year.yday
323
+ end
324
+
325
+ def year
326
+ options.fetch(:year, Date.current.year)
327
+ end
328
+
329
+ def beginning_of_year
330
+ Date.new(year).beginning_of_year
331
+ end
332
+
333
+ def end_of_year
334
+ Date.new(year).end_of_year
335
+ end
336
+ end
337
+ ```
338
+
339
+ #### Usage
340
+
341
+ ```ruby
342
+ hotel = Hotel.find(123)
343
+ AverageHotelDailyRevenue.result_for(hotel)
344
+ AverageHotelDailyRevenue.result_for(hotel, year: 2015)
345
+
346
+ TotalCurrentRevenue.calculate
347
+ AverageDailyRevenue.result
171
348
  ```
172
349
 
173
350
  ## Further reading