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.
data/Rakefile CHANGED
@@ -21,6 +21,7 @@ Juwelier::Tasks.new do |gem|
21
21
  gem.description = "A collection of lightweight, standardized, rails-oriented patterns."
22
22
  gem.email = "b.kosmowski@selleo.com"
23
23
  gem.authors = ["Stevo"]
24
+ gem.required_ruby_version = ">= 2.5.0"
24
25
 
25
26
  # dependencies defined in Gemfile
26
27
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.7.3
@@ -0,0 +1,59 @@
1
+ require 'digest'
2
+
3
+ module Patterns
4
+ class Calculation
5
+ class_attribute :cache_expiry_every
6
+
7
+ def initialize(*args)
8
+ @options = args.extract_options!
9
+ @subject = args.first
10
+ end
11
+
12
+ def self.result(*args)
13
+ new(*args).cached_result
14
+ end
15
+
16
+ class << self
17
+ alias_method :result_for, :result
18
+ alias_method :calculate, :result
19
+ end
20
+
21
+ def self.set_cache_expiry_every(period)
22
+ self.cache_expiry_every = period
23
+ end
24
+
25
+ def cached_result
26
+ if cache_expiry_period.blank?
27
+ result
28
+ else
29
+ Rails.cache.fetch(cache_key, expires_in: cache_expiry_period) do
30
+ result
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :subject, :options
38
+
39
+ def result
40
+ raise NotImplementedError
41
+ end
42
+
43
+ def cache_key
44
+ "#{self.class.name}_#{hash_of(subject, options)}"
45
+ end
46
+
47
+ def self.hash_of(*args)
48
+ Digest::SHA1.hexdigest(args.map(&:to_s).join(':'))
49
+ end
50
+
51
+ def hash_of(*args)
52
+ self.class.hash_of(*args)
53
+ end
54
+
55
+ def cache_expiry_period
56
+ self.class.cache_expiry_every
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,117 @@
1
+ require "virtus"
2
+ require "action_controller/metal/strong_parameters"
3
+
4
+ module Patterns
5
+ class Form
6
+ include Virtus.model
7
+ include ActiveModel::Validations
8
+
9
+ Error = Class.new(StandardError)
10
+ Invalid = Class.new(Error)
11
+ NoParamKey = Class.new(Error)
12
+
13
+ def initialize(*args)
14
+ attributes = args.extract_options!
15
+
16
+ if attributes.blank? && args.last.is_a?(ActionController::Parameters)
17
+ attributes = args.pop.to_unsafe_h
18
+ end
19
+
20
+ @resource = args.first
21
+
22
+ super(build_original_attributes.merge(attributes))
23
+ end
24
+
25
+ def save
26
+ valid? ? persist : false
27
+ end
28
+
29
+ def save!
30
+ save.tap do |saved|
31
+ raise Invalid unless saved
32
+ end
33
+ end
34
+
35
+ def as(form_owner)
36
+ @form_owner = form_owner
37
+ self
38
+ end
39
+
40
+ def to_key
41
+ nil
42
+ end
43
+
44
+ def to_partial_path
45
+ nil
46
+ end
47
+
48
+ def to_model
49
+ self
50
+ end
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
+
60
+ def persisted?
61
+ if resource.present? && resource.respond_to?(:persisted?)
62
+ resource.persisted?
63
+ else
64
+ false
65
+ end
66
+ end
67
+
68
+ def model_name
69
+ @model_name ||= OpenStruct.new(model_name_attributes)
70
+ end
71
+
72
+ def self.param_key(key = nil)
73
+ if key.nil?
74
+ @param_key
75
+ else
76
+ @param_key = key
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ attr_reader :resource, :form_owner
83
+
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
100
+ end
101
+
102
+ def build_original_attributes
103
+ return {} if resource.nil?
104
+ base_attributes = resource.respond_to?(:attributes) && resource.attributes.symbolize_keys
105
+
106
+ self.class.attribute_set.each_with_object(base_attributes || {}) do |attribute, result|
107
+ if result[attribute.name].blank? && resource.respond_to?(attribute.name)
108
+ result[attribute.name] = resource.public_send(attribute.name)
109
+ end
110
+ end
111
+ end
112
+
113
+ def persist
114
+ raise NotImplementedError, "#persist has to be implemented"
115
+ end
116
+ end
117
+ end
@@ -6,17 +6,17 @@ module Patterns
6
6
 
7
7
  def initialize(*args)
8
8
  @options = args.extract_options!
9
- @relation = args.first || self.class.base_relation
9
+ @relation = args.first || base_relation
10
10
 
11
11
  if relation.nil?
12
12
  raise(
13
- RelationRequired,
14
- "Queries require a base relation defined. Use .queries method to define relation."
13
+ RelationRequired,
14
+ "Queries require a base relation defined. Use .queries method to define relation."
15
15
  )
16
16
  elsif !relation.is_a?(ActiveRecord::Relation)
17
17
  raise(
18
- RelationRequired,
19
- "Queries accept only ActiveRecord::Relation as input"
18
+ RelationRequired,
19
+ "Queries accept only ActiveRecord::Relation as input"
20
20
  )
21
21
  end
22
22
  end
@@ -29,20 +29,25 @@ module Patterns
29
29
  query.tap do |relation|
30
30
  unless relation.is_a?(ActiveRecord::Relation)
31
31
  raise(
32
- RelationRequired,
33
- "#query method should return object of ActiveRecord::Relation class"
32
+ RelationRequired,
33
+ "#query method should return object of ActiveRecord::Relation class"
34
34
  )
35
35
  end
36
36
  end
37
37
  end
38
38
 
39
39
  def self.queries(subject)
40
- self.base_relation =
41
- if subject.is_a?(ActiveRecord::Relation)
42
- subject
43
- elsif subject < ActiveRecord::Base
44
- subject.all
45
- end
40
+ self.base_relation = subject
41
+ end
42
+
43
+ def base_relation
44
+ return nil if self.class.base_relation.nil?
45
+
46
+ if self.class.base_relation.is_a?(ActiveRecord::Relation)
47
+ self.class.base_relation
48
+ elsif self.class.base_relation < ActiveRecord::Base
49
+ self.class.base_relation.all
50
+ end
46
51
  end
47
52
 
48
53
  private
@@ -55,8 +60,8 @@ module Patterns
55
60
 
56
61
  def query
57
62
  raise(
58
- NotImplementedError,
59
- "You need to implement #query method which returns ActiveRecord::Relation object"
63
+ NotImplementedError,
64
+ "You need to implement #query method which returns ActiveRecord::Relation object"
60
65
  )
61
66
  end
62
67
  end
@@ -1,13 +1,17 @@
1
+ require 'ruby2_keywords'
2
+
1
3
  module Patterns
2
4
  class Service
3
5
  attr_reader :result
4
6
 
5
- def self.call(*args)
6
- new(*args).tap do |service|
7
- service.instance_variable_set(
8
- "@result",
9
- service.call
10
- )
7
+ class << self
8
+ ruby2_keywords def call(*args)
9
+ new(*args).tap do |service|
10
+ service.instance_variable_set(
11
+ "@result",
12
+ service.call
13
+ )
14
+ end
11
15
  end
12
16
  end
13
17
 
@@ -2,3 +2,5 @@ require "patterns"
2
2
  require "patterns/query"
3
3
  require "patterns/service"
4
4
  require "patterns/collection"
5
+ require "patterns/calculation"
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.3.0 ruby lib
5
+ # stub: rails-patterns 0.7.3 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "rails-patterns".freeze
9
- s.version = "0.3.0"
9
+ s.version = "0.7.3"
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-04-19"
14
+ s.date = "2020-06-12"
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 = [
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
  ]
21
21
  s.files = [
22
22
  ".document",
23
+ ".github/workflows/ruby.yml",
23
24
  ".rspec",
24
25
  "Gemfile",
25
26
  "Gemfile.lock",
@@ -28,19 +29,27 @@ Gem::Specification.new do |s|
28
29
  "Rakefile",
29
30
  "VERSION",
30
31
  "lib/patterns.rb",
32
+ "lib/patterns/calculation.rb",
31
33
  "lib/patterns/collection.rb",
34
+ "lib/patterns/form.rb",
32
35
  "lib/patterns/query.rb",
33
36
  "lib/patterns/service.rb",
34
37
  "lib/rails-patterns.rb",
35
38
  "rails-patterns.gemspec",
39
+ "spec/helpers/custom_calculation.rb",
40
+ "spec/helpers/custom_calculation_script.rb",
41
+ "spec/helpers/rails_redis_cache_mock.rb",
42
+ "spec/patterns/calculation_spec.rb",
36
43
  "spec/patterns/collection_spec.rb",
44
+ "spec/patterns/form_spec.rb",
37
45
  "spec/patterns/query_spec.rb",
38
46
  "spec/patterns/service_spec.rb",
39
47
  "spec/spec_helper.rb"
40
48
  ]
41
49
  s.homepage = "http://github.com/selleo/pattern".freeze
42
50
  s.licenses = ["MIT".freeze]
43
- s.rubygems_version = "2.6.10".freeze
51
+ s.required_ruby_version = Gem::Requirement.new(">= 2.5.0".freeze)
52
+ s.rubygems_version = "2.7.6.2".freeze
44
53
  s.summary = "A collection of lightweight, standardized, rails-oriented patterns.".freeze
45
54
 
46
55
  if s.respond_to? :specification_version then
@@ -48,19 +57,28 @@ Gem::Specification.new do |s|
48
57
 
49
58
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
59
  s.add_runtime_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
60
+ s.add_runtime_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
61
+ s.add_runtime_dependency(%q<virtus>.freeze, [">= 0"])
62
+ s.add_runtime_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
51
63
  s.add_development_dependency(%q<rspec>.freeze, [">= 0"])
52
- s.add_development_dependency(%q<bundler>.freeze, ["~> 1.0"])
64
+ s.add_development_dependency(%q<bundler>.freeze, ["~> 2.0"])
53
65
  s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
54
66
  else
55
67
  s.add_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
68
+ s.add_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
69
+ s.add_dependency(%q<virtus>.freeze, [">= 0"])
70
+ s.add_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
56
71
  s.add_dependency(%q<rspec>.freeze, [">= 0"])
57
- s.add_dependency(%q<bundler>.freeze, ["~> 1.0"])
72
+ s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
58
73
  s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
59
74
  end
60
75
  else
61
76
  s.add_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
77
+ s.add_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
78
+ s.add_dependency(%q<virtus>.freeze, [">= 0"])
79
+ s.add_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
62
80
  s.add_dependency(%q<rspec>.freeze, [">= 0"])
63
- s.add_dependency(%q<bundler>.freeze, ["~> 1.0"])
81
+ s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
64
82
  s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
65
83
  end
66
84
  end
@@ -0,0 +1,16 @@
1
+ require 'active_support/all'
2
+ require 'active_support/testing/time_helpers'
3
+ require_relative 'rails_redis_cache_mock'
4
+ require_relative '../../lib/patterns/calculation'
5
+
6
+ CustomCalculation = Class.new(Patterns::Calculation) do
7
+ set_cache_expiry_every 1.hour
8
+ class_attribute :counter
9
+ self.counter = 0
10
+
11
+ private
12
+
13
+ def result
14
+ self.class.counter += 1
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'rails_redis_cache_mock'
2
+ require_relative 'custom_calculation'
3
+
4
+ CustomCalculation.result
@@ -0,0 +1,5 @@
1
+ class Rails
2
+ def self.cache
3
+ @cache ||= ActiveSupport::Cache::RedisCacheStore.new
4
+ end
5
+ end
@@ -0,0 +1,200 @@
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
+ ActiveSupport::Cache::RedisCacheStore.new.clear
18
+ end
19
+
20
+ describe ".result" do
21
+ it "returns a result of the calculation within a #result method" do
22
+ CustomCalculation = Class.new(Patterns::Calculation) do
23
+ private
24
+
25
+ def result
26
+ 50
27
+ end
28
+ end
29
+
30
+ expect(CustomCalculation.result).to eq 50
31
+ end
32
+
33
+ it "#result, #result_for and #calculate are aliases" do
34
+ CustomCalculation = Class.new(Patterns::Calculation)
35
+
36
+ expect(CustomCalculation.method(:result)).to eq CustomCalculation.method(:result_for)
37
+ expect(CustomCalculation.method(:result)).to eq CustomCalculation.method(:calculate)
38
+ end
39
+
40
+ it "exposes the first argument as a subject" do
41
+ CustomCalculation = Class.new(Patterns::Calculation) do
42
+ private
43
+
44
+ def result
45
+ subject
46
+ end
47
+ end
48
+
49
+ expect(CustomCalculation.result('test')).to eq 'test'
50
+ end
51
+
52
+ it "exposes all keyword arguments using #options" do
53
+ CustomCalculation = Class.new(Patterns::Calculation) do
54
+ private
55
+
56
+ def result
57
+ [options[:arg_1], options[:arg_2]]
58
+ end
59
+ end
60
+
61
+ expect(CustomCalculation.result(nil, arg_1: 20, arg_2: 30)).to eq([20, 30])
62
+ end
63
+ end
64
+
65
+ describe "caching" do
66
+ it "caches result for 'set_cache_expiry_every' period" do
67
+ travel_to DateTime.new(2017, 1, 1, 12, 0) do
68
+ CustomCalculation = Class.new(Patterns::Calculation) do
69
+ set_cache_expiry_every 1.hour
70
+
71
+ class_attribute :counter
72
+ self.counter = 0
73
+
74
+ private
75
+
76
+ def result
77
+ self.class.counter += 1
78
+ end
79
+ end
80
+
81
+ expect(CustomCalculation.result).to eq 1
82
+ expect(CustomCalculation.result).to eq 1
83
+ end
84
+
85
+ travel_to DateTime.new(2017, 1, 1, 13, 1) do
86
+ expect(CustomCalculation.result).to eq 2
87
+ expect(CustomCalculation.result).to eq 2
88
+ end
89
+ end
90
+
91
+ it "caches result for every option passed" do
92
+ CustomCalculation = Class.new(Patterns::Calculation) do
93
+ set_cache_expiry_every 1.hour
94
+
95
+ class_attribute :counter
96
+ self.counter = 0
97
+
98
+ private
99
+
100
+ def result
101
+ self.class.counter += 1
102
+ end
103
+ end
104
+
105
+ expect(CustomCalculation.result(123)).to eq 1
106
+ expect(CustomCalculation.result(123)).to eq 1
107
+ expect(CustomCalculation.result(1024)).to eq 2
108
+ expect(CustomCalculation.result(1024)).to eq 2
109
+ expect(CustomCalculation.result(1024, arg: 1)).to eq 3
110
+ expect(CustomCalculation.result(1024, arg: 1)).to eq 3
111
+ end
112
+
113
+ it "caches result for every option passed dependant on the class" do
114
+ CustomCalculation = Class.new(Patterns::Calculation) do
115
+ set_cache_expiry_every 1.hour
116
+
117
+ class_attribute :counter
118
+ self.counter = 0
119
+
120
+ private
121
+
122
+ def result
123
+ self.class.counter += 1
124
+ end
125
+ end
126
+
127
+ DifferentCalculation = Class.new(Patterns::Calculation) do
128
+ set_cache_expiry_every 1.hour
129
+
130
+ class_attribute :counter
131
+ self.counter = 100
132
+
133
+ private
134
+
135
+ def result
136
+ self.class.counter += 1
137
+ end
138
+ end
139
+
140
+ expect(CustomCalculation.result(123)).to eq 1
141
+ expect(CustomCalculation.result(123)).to eq 1
142
+ expect(DifferentCalculation.result(123)).to eq 101
143
+ expect(DifferentCalculation.result(123)).to eq 101
144
+
145
+ Object.send(:remove_const, :DifferentCalculation)
146
+ end
147
+
148
+ it "does not cache result if 'set_cache_expiry_every' is not set" do
149
+ CustomCalculation = Class.new(Patterns::Calculation) do
150
+ class_attribute :counter
151
+ self.counter = 0
152
+
153
+ private
154
+
155
+ def result
156
+ self.class.counter += 1
157
+ end
158
+ end
159
+
160
+ expect(CustomCalculation.result).to eq 1
161
+ expect(CustomCalculation.result).to eq 2
162
+ end
163
+
164
+ describe "when RedisCacheStore is used" do
165
+ it "does not store data in cache if 'cache_expiry_period' is not set" do
166
+ client = Redis.new
167
+ class Rails
168
+ def self.cache
169
+ @cache ||= ActiveSupport::Cache::RedisCacheStore.new
170
+ end
171
+ end
172
+
173
+ CustomCalculation = Class.new(Patterns::Calculation) do
174
+ class_attribute :counter
175
+ self.counter = 0
176
+
177
+ private
178
+
179
+ def result
180
+ self.class.counter += 1
181
+ end
182
+ end
183
+
184
+ expect(CustomCalculation.result).to eq 1
185
+ expect(CustomCalculation.result).to eq 2
186
+ expect(client.keys).to be_empty
187
+ end
188
+ end
189
+
190
+ it "uses cache keys consistent between processes" do
191
+ `bundle exec ruby spec/helpers/custom_calculation.rb`
192
+ Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
193
+ Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
194
+ Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
195
+ Process.waitall
196
+
197
+ expect(Redis.new.keys.length).to eq 1
198
+ end
199
+ end
200
+ end