rails-patterns 0.4.1 → 0.5.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
  SHA1:
3
- metadata.gz: c0527dc116049d1579ba19deadf9ad643b0abf9b
4
- data.tar.gz: 52f5f8cae780e21a3a91b7b82602d71814883a3c
3
+ metadata.gz: a0004234c0698b57bbf9960c6431427a1f548df9
4
+ data.tar.gz: 9891b7eba903e69d516153c5664b9cf0e04c7f90
5
5
  SHA512:
6
- metadata.gz: 58005bd76bfb7d2c30d83025343765b201994e1b4161e0e7c04b37a54ed5e88a98180afde2d4c46f8e70e4dd51ad06c0733bd073d47a5647721d7dbda43f0b49
7
- data.tar.gz: b461d4fa96a92acb04a56ccc202a41aa79cbdb2948a0c94f5a56ebc3bd228c277a5db17331c6e690a9bdade2b84fe4a6ae008e835c84556ede60b4a038f991b9
6
+ metadata.gz: 8aa0d32fc28cb5924dab59cc9a9d088e940cb7d11e19058f7d7bd5fdc25084554655ee48b9ce1e5938e21b5a70358b78a2d4aa0a98ee455b44d6502e6f2eb615
7
+ data.tar.gz: 3bcec212d6958aeed3e29ff72ffeca10d0dbe726b1376aa42585f171464f726ac36950db8b49c8a9bfc8eaad98c47458b48a4a01c613905f138759a89c243d61
data/Gemfile.lock CHANGED
@@ -136,4 +136,4 @@ DEPENDENCIES
136
136
  virtus
137
137
 
138
138
  BUNDLED WITH
139
- 1.14.6
139
+ 1.15.1
data/README.md CHANGED
@@ -6,6 +6,7 @@ A collection of lightweight, standardized, rails-oriented patterns.
6
6
  - [Service - useful for handling processes involving multiple steps](#service)
7
7
  - [Collection - when in need to add a method that relates to the collection as whole](#collection)
8
8
  - [Form - when you need a place for callbacks, want to replace strong parameters or handle virtual/composite resources](#form)
9
+ - [Calculation - when you need a place for calculating a simple value (numeric, array, hash) and/or cache it](#calculation)
9
10
 
10
11
  ## Installation
11
12
 
@@ -274,6 +275,75 @@ ReportConfigurationForm.new
274
275
  ReportConfigurationForm.new({ include_extra_data: true, dump_as_csv: true })
275
276
  ```
276
277
 
278
+ ## Calculation
279
+
280
+ ### When to use it
281
+
282
+ 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.
283
+ Calculation objects also provide simple abstraction for caching their results.
284
+
285
+ ### Assumptions and rules
286
+
287
+ * Calculations have to implement `#result` method that returns any value (result of calculation).
288
+ * Calculations do provide `.set_cache_expiry_every` method, that allows defining caching period.
289
+ * When `.set_cache_expiry_every` is not used, result is not being cached.
290
+ * Calculations return result by calling any of following methods: `.calculate`, `.result_for` or `.result`.
291
+ * First argument passed to calculation is accessible by `#subject` private method.
292
+ * Arguments hash passed to calculation is accessible by `#options` private method.
293
+ * Caching takes into account arguments passed when building cache key.
294
+ * To build cache key, `#cache_key` of each argument value is used if possible.
295
+ * By default `Rails.cache` is used as cache store.
296
+
297
+ ### Examples
298
+
299
+ #### Declaration
300
+
301
+ ```ruby
302
+ class AverageHotelDailyRevenue < Patterns::Calculation
303
+ set_cache_expiry_every 1.day
304
+
305
+ private
306
+
307
+ def result
308
+ reservations.sum(:price) / days_in_year
309
+ end
310
+
311
+ def reservations
312
+ Reservation.where(
313
+ date: (beginning_of_year..end_of_year),
314
+ hotel_id: subject.id
315
+ )
316
+ end
317
+
318
+ def days_in_year
319
+ end_of_year.yday
320
+ end
321
+
322
+ def year
323
+ options.fetch(:year, Date.current.year)
324
+ end
325
+
326
+ def beginning_of_year
327
+ Date.new(year).beginning_of_year
328
+ end
329
+
330
+ def end_of_year
331
+ Date.new(year).end_of_year
332
+ end
333
+ end
334
+ ```
335
+
336
+ #### Usage
337
+
338
+ ```ruby
339
+ hotel = Hotel.find(123)
340
+ AverageHotelDailyRevenue.result_for(hotel)
341
+ AverageHotelDailyRevenue.result_for(hotel, year: 2015)
342
+
343
+ TotalCurrentRevenue.calculate
344
+ AverageDailyRevenue.result
345
+ ```
346
+
277
347
  ## Further reading
278
348
 
279
349
  * [7 ways to decompose fat active record models](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.1
1
+ 0.5.0
@@ -0,0 +1,45 @@
1
+ module Patterns
2
+ class Calculation
3
+ class_attribute :cache_expiry_every
4
+
5
+ def initialize(*args)
6
+ @options = args.extract_options!
7
+ @subject = args.first
8
+ end
9
+
10
+ def self.result(*args)
11
+ new(*args).cached_result
12
+ end
13
+
14
+ class << self
15
+ alias_method :result_for, :result
16
+ alias_method :calculate, :result
17
+ end
18
+
19
+ def self.set_cache_expiry_every(period)
20
+ self.cache_expiry_every = period
21
+ end
22
+
23
+ def cached_result
24
+ Rails.cache.fetch(cache_key, expires_in: cache_expiry_period, force: cache_expiry_period.blank?) do
25
+ result
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :subject, :options
32
+
33
+ def result
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def cache_key
38
+ "#{self.class.name}_#{[subject, options].hash}"
39
+ end
40
+
41
+ def cache_expiry_period
42
+ self.class.cache_expiry_every
43
+ end
44
+ end
45
+ end
data/lib/patterns/form.rb CHANGED
@@ -49,6 +49,14 @@ module Patterns
49
49
  self
50
50
  end
51
51
 
52
+ def to_param
53
+ if resource.present? && resource.respond_to?(:to_param)
54
+ resource.to_param
55
+ else
56
+ nil
57
+ end
58
+ end
59
+
52
60
  def persisted?
53
61
  if resource.present? && resource.respond_to?(:persisted?)
54
62
  resource.persisted?
@@ -58,9 +66,7 @@ module Patterns
58
66
  end
59
67
 
60
68
  def model_name
61
- @model_name ||= Struct.
62
- new(:param_key).
63
- new(param_key)
69
+ @model_name ||= OpenStruct.new(model_name_attributes)
64
70
  end
65
71
 
66
72
  def self.param_key(key = nil)
@@ -75,13 +81,24 @@ module Patterns
75
81
 
76
82
  attr_reader :resource, :form_owner
77
83
 
78
- def param_key
79
- param_key = self.class.param_key
80
- param_key ||= resource.present? && resource.respond_to?(:model_name) && resource.model_name.param_key
81
- raise NoParamKey if param_key.blank?
82
- param_key
84
+ def model_name_attributes
85
+ if self.class.param_key.present?
86
+ {
87
+ param_key: self.class.param_key,
88
+ route_key: self.class.param_key.pluralize,
89
+ singular_route_key: self.class.param_key
90
+ }
91
+ elsif resource.present? && resource.respond_to?(:model_name)
92
+ {
93
+ param_key: resource.model_name.param_key,
94
+ route_key: resource.model_name.route_key,
95
+ singular_route_key: resource.model_name.singular_route_key
96
+ }
97
+ else
98
+ raise NoParamKey
99
+ end
83
100
  end
84
-
101
+
85
102
  def build_original_attributes
86
103
  return {} if resource.nil?
87
104
  base_attributes = resource.respond_to?(:attributes) && resource.attributes.symbolize_keys
@@ -2,4 +2,5 @@ require "patterns"
2
2
  require "patterns/query"
3
3
  require "patterns/service"
4
4
  require "patterns/collection"
5
+ require "patterns/calculation"
5
6
  require "patterns/form"
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: rails-patterns 0.4.1 ruby lib
5
+ # stub: rails-patterns 0.5.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "rails-patterns".freeze
9
- s.version = "0.4.1"
9
+ s.version = "0.5.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Stevo".freeze]
14
- s.date = "2017-05-09"
14
+ s.date = "2017-06-22"
15
15
  s.description = "A collection of lightweight, standardized, rails-oriented patterns.".freeze
16
16
  s.email = "b.kosmowski@selleo.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -28,12 +28,14 @@ Gem::Specification.new do |s|
28
28
  "Rakefile",
29
29
  "VERSION",
30
30
  "lib/patterns.rb",
31
+ "lib/patterns/calculation.rb",
31
32
  "lib/patterns/collection.rb",
32
33
  "lib/patterns/form.rb",
33
34
  "lib/patterns/query.rb",
34
35
  "lib/patterns/service.rb",
35
36
  "lib/rails-patterns.rb",
36
37
  "rails-patterns.gemspec",
38
+ "spec/patterns/calculation_spec.rb",
37
39
  "spec/patterns/collection_spec.rb",
38
40
  "spec/patterns/form_spec.rb",
39
41
  "spec/patterns/query_spec.rb",
@@ -0,0 +1,163 @@
1
+ RSpec.describe Patterns::Calculation do
2
+ before(:all) do
3
+ class Rails
4
+ def self.cache
5
+ @cache ||= ActiveSupport::Cache::MemoryStore.new
6
+ end
7
+ end
8
+ end
9
+
10
+ after(:all) do
11
+ Object.send(:remove_const, :Rails)
12
+ end
13
+
14
+ after do
15
+ Object.send(:remove_const, :CustomCalculation) if defined?(CustomCalculation)
16
+ Rails.cache.clear
17
+ end
18
+
19
+ describe ".result" do
20
+ it "returns a result of the calculation within a #result method" do
21
+ CustomCalculation = Class.new(Patterns::Calculation) do
22
+ private
23
+
24
+ def result
25
+ 50
26
+ end
27
+ end
28
+
29
+ expect(CustomCalculation.result).to eq 50
30
+ end
31
+
32
+ it "#result, #result_for and #calculate are aliases" do
33
+ CustomCalculation = Class.new(Patterns::Calculation)
34
+
35
+ expect(CustomCalculation.method(:result)).to eq CustomCalculation.method(:result_for)
36
+ expect(CustomCalculation.method(:result)).to eq CustomCalculation.method(:calculate)
37
+ end
38
+
39
+ it "exposes the first argument as a subject" do
40
+ CustomCalculation = Class.new(Patterns::Calculation) do
41
+ private
42
+
43
+ def result
44
+ subject
45
+ end
46
+ end
47
+
48
+ expect(CustomCalculation.result('test')).to eq 'test'
49
+ end
50
+
51
+ it "exposes all keyword arguments using #options" do
52
+ CustomCalculation = Class.new(Patterns::Calculation) do
53
+ private
54
+
55
+ def result
56
+ [options[:arg_1], options[:arg_2]]
57
+ end
58
+ end
59
+
60
+ expect(CustomCalculation.result(nil, arg_1: 20, arg_2: 30)).to eq([20, 30])
61
+ end
62
+ end
63
+
64
+ describe "caching" do
65
+ it "caches result for 'set_cache_expiry_every' period" do
66
+ travel_to DateTime.new(2017, 1, 1, 12, 0) do
67
+ CustomCalculation = Class.new(Patterns::Calculation) do
68
+ set_cache_expiry_every 1.hour
69
+
70
+ class_attribute :counter
71
+ self.counter = 0
72
+
73
+ private
74
+
75
+ def result
76
+ self.class.counter += 1
77
+ end
78
+ end
79
+
80
+ expect(CustomCalculation.result).to eq 1
81
+ expect(CustomCalculation.result).to eq 1
82
+ end
83
+
84
+ travel_to DateTime.new(2017, 1, 1, 13, 1) do
85
+ expect(CustomCalculation.result).to eq 2
86
+ expect(CustomCalculation.result).to eq 2
87
+ end
88
+ end
89
+
90
+ it "caches result for every option passed" do
91
+ CustomCalculation = Class.new(Patterns::Calculation) do
92
+ set_cache_expiry_every 1.hour
93
+
94
+ class_attribute :counter
95
+ self.counter = 0
96
+
97
+ private
98
+
99
+ def result
100
+ self.class.counter += 1
101
+ end
102
+ end
103
+
104
+ expect(CustomCalculation.result(123)).to eq 1
105
+ expect(CustomCalculation.result(123)).to eq 1
106
+ expect(CustomCalculation.result(1024)).to eq 2
107
+ expect(CustomCalculation.result(1024)).to eq 2
108
+ expect(CustomCalculation.result(1024, arg: 1)).to eq 3
109
+ expect(CustomCalculation.result(1024, arg: 1)).to eq 3
110
+ end
111
+
112
+ it "caches result for every option passed dependant on the class" do
113
+ CustomCalculation = Class.new(Patterns::Calculation) do
114
+ set_cache_expiry_every 1.hour
115
+
116
+ class_attribute :counter
117
+ self.counter = 0
118
+
119
+ private
120
+
121
+ def result
122
+ self.class.counter += 1
123
+ end
124
+ end
125
+
126
+ DifferentCalculation = Class.new(Patterns::Calculation) do
127
+ set_cache_expiry_every 1.hour
128
+
129
+ class_attribute :counter
130
+ self.counter = 100
131
+
132
+ private
133
+
134
+ def result
135
+ self.class.counter += 1
136
+ end
137
+ end
138
+
139
+ expect(CustomCalculation.result(123)).to eq 1
140
+ expect(CustomCalculation.result(123)).to eq 1
141
+ expect(DifferentCalculation.result(123)).to eq 101
142
+ expect(DifferentCalculation.result(123)).to eq 101
143
+
144
+ Object.send(:remove_const, :DifferentCalculation)
145
+ end
146
+
147
+ it "does not cache result if 'set_cache_expiry_every' is not set" do
148
+ CustomCalculation = Class.new(Patterns::Calculation) do
149
+ class_attribute :counter
150
+ self.counter = 0
151
+
152
+ private
153
+
154
+ def result
155
+ self.class.counter += 1
156
+ end
157
+ end
158
+
159
+ expect(CustomCalculation.result).to eq 1
160
+ expect(CustomCalculation.result).to eq 2
161
+ end
162
+ end
163
+ end
@@ -316,7 +316,7 @@ RSpec.describe Patterns::Form do
316
316
  end
317
317
 
318
318
  describe "#to_model" do
319
- it "retruns itself" do
319
+ it "returns itself" do
320
320
  CustomForm = Class.new(Patterns::Form)
321
321
 
322
322
  form = CustomForm.new(double)
@@ -345,86 +345,132 @@ RSpec.describe Patterns::Form do
345
345
  end
346
346
  end
347
347
 
348
- describe "#model_name" do
349
- describe "#param_key" do
350
- context "resource exists" do
351
- context "resource responds to #model_name" do
352
- context "param_key is not defined" do
353
- it "returns object responding to #param_key returning resource#param_key" do
354
- CustomForm = Class.new(Patterns::Form)
355
- resource = double(model_name: double(param_key: "resource_key"))
356
-
357
- form = CustomForm.new(resource)
358
- result = form.model_name
359
-
360
- expect(result).to respond_to(:param_key)
361
- expect(result.param_key).to eq "resource_key"
362
- end
363
- end
364
-
365
- context "param_key is defined" do
366
- it "returns param_key" do
367
- CustomForm = Class.new(Patterns::Form) do
368
- param_key "test_key"
369
- end
370
- resource = double(model_name: double(param_key: "resource_key"))
348
+ describe "#to_param" do
349
+ context "resource exists" do
350
+ context "resource responds to #to_param" do
351
+ it "returns resource#to_param" do
352
+ CustomForm = Class.new(Patterns::Form)
353
+ resource = double(to_param: 100)
371
354
 
372
- form = CustomForm.new(resource)
373
- result = form.model_name
355
+ form = CustomForm.new(resource)
374
356
 
375
- expect(result.param_key).to eq "test_key"
376
- end
377
- end
357
+ expect(form.to_param).to eq 100
378
358
  end
359
+ end
360
+ end
361
+
362
+ context "resource does not exist" do
363
+ it "returns nil" do
364
+ CustomForm = Class.new(Patterns::Form)
379
365
 
380
- context "resource does not respond to #model_name" do
381
- context "param_key is not defined" do
382
- it "raises NoParamKey" do
383
- CustomForm = Class.new(Patterns::Form)
366
+ form = CustomForm.new
384
367
 
385
- form = CustomForm.new(double)
368
+ expect(form.to_param).to eq nil
369
+ end
370
+ end
371
+ end
386
372
 
387
- expect { form.model_name }.to raise_error(Patterns::Form::NoParamKey)
388
- end
389
- end
373
+ describe "#model_name" do
374
+ context "resource exists" do
375
+ context "resource responds to #model_name" do
376
+ context "param_key is not defined" do
377
+ it "returns object's model name param_key, route_key and singular_route_key" do
378
+ CustomForm = Class.new(Patterns::Form)
379
+ resource = double(model_name: double(
380
+ param_key: "resource_key",
381
+ route_key: "resource_keys",
382
+ singular_route_key: "resource_key"
383
+ ))
390
384
 
391
- context "param_key is defined" do
392
- it "returns param_key" do
393
- CustomForm = Class.new(Patterns::Form) do
394
- param_key "test_key"
395
- end
385
+ form = CustomForm.new(resource)
386
+ result = form.model_name
396
387
 
397
- form = CustomForm.new(double)
398
- result = form.model_name
388
+ expect(result).to have_attributes(
389
+ param_key: "resource_key",
390
+ route_key: "resource_keys",
391
+ singular_route_key: "resource_key"
392
+ )
393
+ end
394
+ end
399
395
 
400
- expect(result.param_key).to eq "test_key"
396
+ context "param_key is defined" do
397
+ it "returns param_key, route_key and singular_route_key derived from param key" do
398
+ CustomForm = Class.new(Patterns::Form) do
399
+ param_key "test_key"
401
400
  end
401
+ resource = double(model_name: double(
402
+ param_key: "resource_key",
403
+ route_key: "resource_keys",
404
+ singular_route_key: "resource_key"
405
+ ))
406
+
407
+ form = CustomForm.new(resource)
408
+ result = form.model_name
409
+
410
+ expect(result).to have_attributes(
411
+ param_key: "test_key",
412
+ route_key: "test_keys",
413
+ singular_route_key: "test_key"
414
+ )
402
415
  end
403
416
  end
404
417
  end
405
418
 
406
- context "resource does not exist" do
419
+ context "resource does not respond to #model_name" do
407
420
  context "param_key is not defined" do
408
421
  it "raises NoParamKey" do
409
422
  CustomForm = Class.new(Patterns::Form)
410
423
 
411
- form = CustomForm.new
424
+ form = CustomForm.new(double)
412
425
 
413
426
  expect { form.model_name }.to raise_error(Patterns::Form::NoParamKey)
414
427
  end
415
428
  end
416
429
 
417
430
  context "param_key is defined" do
418
- it "returns param_key" do
431
+ it "returns param_key, route_key and singular_route_key derived from param key" do
419
432
  CustomForm = Class.new(Patterns::Form) do
420
433
  param_key "test_key"
421
434
  end
422
435
 
423
- form = CustomForm.new
436
+ form = CustomForm.new(double)
424
437
  result = form.model_name
425
438
 
426
- expect(result.param_key).to eq "test_key"
439
+ expect(result).to have_attributes(
440
+ param_key: "test_key",
441
+ route_key: "test_keys",
442
+ singular_route_key: "test_key"
443
+ )
444
+ end
445
+ end
446
+ end
447
+ end
448
+
449
+ context "resource does not exist" do
450
+ context "param_key is not defined" do
451
+ it "raises NoParamKey" do
452
+ CustomForm = Class.new(Patterns::Form)
453
+
454
+ form = CustomForm.new
455
+
456
+ expect { form.model_name }.to raise_error(Patterns::Form::NoParamKey)
457
+ end
458
+ end
459
+
460
+ context "param_key is defined" do
461
+ it "returns param_key, route_key and singular_route_key derived from param key" do
462
+ CustomForm = Class.new(Patterns::Form) do
463
+ param_key "test_key"
427
464
  end
465
+
466
+ form = CustomForm.new
467
+ result = form.model_name
468
+
469
+ expect(result).to have_attributes(
470
+ param_key: "test_key",
471
+ route_key: "test_keys",
472
+ singular_route_key: "test_key"
473
+ )
428
474
  end
429
475
  end
430
476
  end
data/spec/spec_helper.rb CHANGED
@@ -17,10 +17,14 @@
17
17
  #
18
18
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
19
19
 
20
- require "rails-patterns"
20
+ require "active_support/all"
21
+ require "active_support/testing/time_helpers"
21
22
  require "pry"
23
+ require "rails-patterns"
22
24
 
23
25
  RSpec.configure do |config|
26
+ config.include ActiveSupport::Testing::TimeHelpers
27
+
24
28
  # rspec-expectations config goes here. You can use an alternate
25
29
  # assertion/expectation library such as wrong or the stdlib/minitest
26
30
  # assertions if you prefer.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-patterns
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stevo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-09 00:00:00.000000000 Z
11
+ date: 2017-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -111,12 +111,14 @@ files:
111
111
  - Rakefile
112
112
  - VERSION
113
113
  - lib/patterns.rb
114
+ - lib/patterns/calculation.rb
114
115
  - lib/patterns/collection.rb
115
116
  - lib/patterns/form.rb
116
117
  - lib/patterns/query.rb
117
118
  - lib/patterns/service.rb
118
119
  - lib/rails-patterns.rb
119
120
  - rails-patterns.gemspec
121
+ - spec/patterns/calculation_spec.rb
120
122
  - spec/patterns/collection_spec.rb
121
123
  - spec/patterns/form_spec.rb
122
124
  - spec/patterns/query_spec.rb