rails-patterns 0.4.1 → 0.5.0

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