rails-patterns 0.3.0 → 0.7.3

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