gourami 0.1.2 → 0.2.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: 5a514a33e36dee0b4a2c39ef0320f4a8fa70ab24
4
- data.tar.gz: 6acabd9872fd0346833fa7f50b527d1413a202f0
3
+ metadata.gz: f97edb63e52af97e6e63f41a74a824a04da00c21
4
+ data.tar.gz: 74bff6e2f3563c96205fc155f0b21db04df37169
5
5
  SHA512:
6
- metadata.gz: 97e2db4cccf243740e2e1548673df9860a06aeb856516c77e9596bab2ff06c39c86c982266a5cbe24d33b6383d885cc1e4411d0fb934c7570628de2000edbfa7
7
- data.tar.gz: 15cff403f9f9a2d2a7536278fa114fff0f503e8a13e539546f8ae049d978c76231c71fe7e60c5c65721b4761d2178bd6dd4340be5fbd23f3f472f635a6111f35
6
+ metadata.gz: f2ea15a122976499b06265949d859767170c66d7647bac35861a9bea9e8b95f2090da92bd8f6ee99f7677c59eb5ce47ab59d25e21922a2a6a2dd4698cd279d4e
7
+ data.tar.gz: a38f7c552c0939c3122171945e855e4edff4482206e8c4e1a5b82d9100a92d1de2ce462bffbee147804fdb763ecc042dd550f441cee909f26b54922e2cd3e78c
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Gourami
2
2
 
3
- Keep your Routes, Controllers and Models thin.
3
+ Keep your Routes, Controllers and Models thin with Plain Old Ruby Objects (PORO).
4
4
 
5
5
  ## Installation
6
6
 
@@ -22,9 +22,9 @@ Or install it yourself as:
22
22
 
23
23
  ### A Typical Gourami::Form will
24
24
 
25
- - list a set of attributes
26
- - validate user input
27
- - perform an action
25
+ - Define some attributes
26
+ - Validate user input
27
+ - Perform an action
28
28
 
29
29
  ```ruby
30
30
  class TypicalForm < Gourami::Form
@@ -32,7 +32,7 @@ class TypicalForm < Gourami::Form
32
32
  attribute(:typical_attribute)
33
33
 
34
34
  def validate
35
- # Define Your validation rules here
35
+ # Define your validation rules here
36
36
  end
37
37
 
38
38
  def perform
@@ -67,7 +67,6 @@ end
67
67
  class CreateFishBowl < Gourami::Form
68
68
 
69
69
  record(:fish_bowl)
70
-
71
70
  attribute(:width, type: :integer)
72
71
  attribute(:height, type: :integer)
73
72
  attribute(:liters, type: :float)
@@ -120,10 +119,9 @@ end
120
119
  ### Example of a form that Updates a record
121
120
 
122
121
  ```ruby
123
- class UpdateFishBowl < CreateFishBowl
122
+ class UpdateFishBowl < Gourami::Form
124
123
 
125
124
  record(:fish_bowl)
126
-
127
125
  attribute(:width, type: :integer)
128
126
  attribute(:height, type: :integer)
129
127
  attribute(:liters, type: :float)
@@ -131,7 +129,7 @@ class UpdateFishBowl < CreateFishBowl
131
129
  attribute(:filter_included, type: :boolean, default: false)
132
130
 
133
131
  def self.new_from_record(fish_bowl)
134
- new({ fish_bowl: fish_bowl }.merge(fish_bowl.attributes))
132
+ new(fish_bowl.attributes.merge(fish_bowl: fish_bowl))
135
133
  end
136
134
 
137
135
  def validate
@@ -165,7 +163,7 @@ class UpdateFishBowl < CreateFishBowl
165
163
  # All attributes and validations inherited from CreateFishBowl.
166
164
 
167
165
  def self.new_from_record(fish_bowl)
168
- new({ fish_bowl: fish_bowl }.merge(fish_bowl.attributes))
166
+ new(fish_bowl.attributes.merge(fish_bowl: fish_bowl))
169
167
  end
170
168
 
171
169
  def perform
@@ -175,6 +173,81 @@ class UpdateFishBowl < CreateFishBowl
175
173
  end
176
174
  ```
177
175
 
176
+ #### Extensions / Plugins
177
+
178
+ ##### Gourami::Extensions::Changes
179
+
180
+ Check to see if an attribute is being changed:
181
+
182
+ ```ruby
183
+ class UpdateUserEmail < Gourami::Form
184
+
185
+ include Gourami::Extensions::Changes
186
+
187
+ record(:user)
188
+ attribute(:email, :type => :string, :watch_changes => true)
189
+
190
+ def perform
191
+ user.update(attributes)
192
+
193
+ do_something_like_send_confirmation_email(email) if changes?(:email)
194
+ end
195
+
196
+ end
197
+ ```
198
+
199
+ ###### You can implement custom logic to determine if an attribute is changing
200
+
201
+ This is the equivalent behavior when you set `:watch_changes => true`
202
+
203
+ ```ruby
204
+ attribute(:email, :watch_changes => ->(new_value) { new_value != user.email })
205
+ ```
206
+
207
+ Your logic to check for changes can be as sophisticated as you want.
208
+
209
+ ```ruby
210
+ class UpdatePageAuthorizedUsers < Gourami::Form
211
+
212
+ include Gourami::Extensions::Changes
213
+
214
+ record(:page)
215
+ attribute(:authorized_users,
216
+ :type => :array,
217
+ :watch_changes => ->(new_value) { new_value.sort.uniq != page.authorized_users.sort.uniq })
218
+
219
+ def perform
220
+ page.update(attributes)
221
+
222
+ do_something_like_notify_authorization_libraries(authorized_users) if changes?(:authorized_users)
223
+ end
224
+
225
+ end
226
+ ```
227
+
228
+ You can also keep track of side effects due to changes by using `did_change`.
229
+
230
+ ```ruby
231
+ class UpdatePageWidgets < Gourami::Form
232
+
233
+ include Gourami::Extensions::Changes
234
+
235
+ record(:page)
236
+ attribute(:widgets,
237
+ :type => :array,
238
+ :element_type => :string,
239
+ :watch_changes => ->(new_value) {
240
+ did_change(:pro_widget, new_value.include?("pro"))
241
+ new_value.sort.uniq != page.widgets.sort.uniq
242
+ })
243
+
244
+ def validate
245
+ append_error(:widgets, :unauthorized) if changes?(:pro_widget) && !current_user_has_pro_account?
246
+ end
247
+
248
+ end
249
+ ```
250
+
178
251
  ## Development
179
252
 
180
253
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -1,12 +1,18 @@
1
1
  module Gourami
2
2
  end
3
3
 
4
+ require "gourami/error"
5
+ require "gourami/configuration_error"
6
+ require "gourami/not_watching_changes_error"
7
+ require "gourami/required_attribute_error"
8
+ require "gourami/validation_error"
9
+ require "gourami/watch_changes_error"
10
+
4
11
  require "gourami/attributes"
5
12
  require "gourami/coercer"
13
+ require "gourami/extensions/changes"
6
14
  require "gourami/extensions/resources"
7
15
  require "gourami/form"
8
16
  require "gourami/formatting_constants"
9
- require "gourami/required_attribute_error"
10
- require "gourami/validation_error"
11
17
  require "gourami/validations"
12
18
  require "gourami/version"
@@ -23,39 +23,42 @@ module Gourami
23
23
  options = options.dup
24
24
  options[:default] = default_block if block_given?
25
25
 
26
- unless options[:skip_reader]
27
- define_method(:"#{name}") do
28
- value = instance_variable_get(:"@#{name}")
29
- default = options[:default]
30
-
31
- if value.nil? && default
32
- default.respond_to?(:call) ? instance_exec(&default) : default
33
- else
34
- value
26
+ mixin = Module.new do |mixin|
27
+ unless options[:skip_reader]
28
+ mixin.send(:define_method, :"#{name}") do
29
+ value = instance_variable_get(:"@#{name}")
30
+ default = options[:default]
31
+
32
+ if value.nil? && default
33
+ default.respond_to?(:call) ? instance_exec(&default) : default
34
+ else
35
+ value
36
+ end
35
37
  end
36
38
  end
37
- end
38
39
 
39
- # Define external setter.
40
- define_method(:"#{name}=") do |value|
41
- provided_attributes_names[name.to_s] = options
42
- send(:"_#{name}=", value)
43
- end
40
+ # Define external setter.
41
+ mixin.send(:define_method, :"#{name}=") do |value|
42
+ provided_attributes_names[name.to_s] = options
43
+ send(:"_#{name}=", value)
44
+ end
44
45
 
45
- # Define internal setter.
46
- define_method(:"_#{name}=") do |value|
47
- value = setter_filter(name, value, options)
48
- instance_variable_set(:"@#{name}", value)
49
- end
50
- private :"_#{name}="
46
+ # Define internal setter.
47
+ mixin.send(:define_method, :"_#{name}=") do |value|
48
+ instance_variable_set(:"@#{name}", setter_filter(name, value, options))
49
+ end
50
+ mixin.send(:private, :"_#{name}=")
51
51
 
52
- case options[:type]
53
- when :boolean
54
- define_method(:"#{name}?") do
55
- !!send(name)
52
+ case options[:type]
53
+ when :boolean
54
+ mixin.send(:define_method, :"#{name}?") do
55
+ !!send(name)
56
+ end
56
57
  end
57
58
  end
58
59
 
60
+ include(mixin)
61
+
59
62
  attributes[name] = options
60
63
  end
61
64
 
@@ -71,7 +74,7 @@ module Gourami
71
74
  define_method(:record) do
72
75
  send(name)
73
76
  end
74
- attribute(name, options.merge(:skip => true))
77
+ attribute(name, options.merge(:skip => true, :record => true))
75
78
  end
76
79
 
77
80
  # Retrieve the list of attributes of the form.
@@ -0,0 +1,4 @@
1
+ module Gourami
2
+ class ConfigurationError < Gourami::Error
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Gourami
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,69 @@
1
+ module Gourami
2
+ module Extensions
3
+ module Changes
4
+ WATCH_CHANGES_VALID_RETURN_VALUES = [true, false].freeze
5
+
6
+ module ClassMethods
7
+ def attribute(name, options = {}, &default_block)
8
+ super.tap do
9
+ watch_changes = options.fetch(:watch_changes, false)
10
+
11
+ if watch_changes
12
+ mixin = Module.new do |mixin|
13
+ mixin.send(:define_method, :"_#{name}=") do |value|
14
+ super(value).tap do
15
+ new_value = instance_variable_get(:"@#{name}")
16
+ attribute_did_change = if watch_changes.respond_to?(:call)
17
+ instance_exec(new_value, &watch_changes)
18
+ elsif !defined?(record)
19
+ raise ConfigurationError, "Default `watch_changes` behavior not available without a `record`. Try `attribute(:#{name}, :watch_changes => ->(new_value) { new_value != custom_check_logic )`"
20
+ elsif record.nil?
21
+ !new_value.nil?
22
+ else
23
+ record.send(name) != new_value
24
+ end
25
+
26
+ unless WATCH_CHANGES_VALID_RETURN_VALUES.include?(attribute_did_change)
27
+ raise WatchChangesError, "`watch_changes` block for `#{name.inspect}` must return one of #{WATCH_CHANGES_VALID_RETURN_VALUES.inspect}."
28
+ end
29
+
30
+ did_change(name, attribute_did_change)
31
+ end
32
+ end
33
+ mixin.send(:private, :"_#{name}=")
34
+ end
35
+ include(mixin)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.included(klass)
42
+ klass.send(:extend, ClassMethods)
43
+ end
44
+
45
+ def changes?(attribute_name)
46
+ attribute_name_sym = attribute_name.to_sym
47
+ changed_attributes.fetch(attribute_name_sym) do
48
+ options = self.class.attributes.fetch(attribute_name_sym, {})
49
+ watch_changes = options.fetch(:watch_changes, false)
50
+
51
+ return false if watch_changes
52
+
53
+ raise NotWatchingChangesError, "`#{attribute_name}` is not being watched for changes. " \
54
+ "Try `attribute(:#{attribute_name}, :watch_changes => true)`"
55
+ end
56
+ end
57
+
58
+ def did_change(attribute_name, changed = true)
59
+ changed_attributes[attribute_name.to_sym] = !!changed
60
+ end
61
+
62
+ private
63
+
64
+ def changed_attributes
65
+ @changed_attributes ||= {}
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,4 @@
1
+ module Gourami
2
+ class NotWatchingChangesError < Gourami::Error
3
+ end
4
+ end
@@ -1,4 +1,4 @@
1
1
  module Gourami
2
- class RequiredAttributeError < StandardError
2
+ class RequiredAttributeError < Gourami::Error
3
3
  end
4
4
  end
@@ -1,5 +1,5 @@
1
1
  module Gourami
2
- class ValidationError < StandardError
2
+ class ValidationError < Gourami::Error
3
3
 
4
4
  def self.stringify_errors(errors)
5
5
  [].tap do |array|
@@ -1,3 +1,3 @@
1
1
  module Gourami
2
- VERSION = "0.1.2".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
@@ -0,0 +1,4 @@
1
+ module Gourami
2
+ class WatchChangesError < Gourami::Error
3
+ end
4
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gourami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TSMMark
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-05-09 00:00:00.000000000 Z
11
+ date: 2017-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -87,13 +87,18 @@ files:
87
87
  - lib/gourami.rb
88
88
  - lib/gourami/attributes.rb
89
89
  - lib/gourami/coercer.rb
90
+ - lib/gourami/configuration_error.rb
91
+ - lib/gourami/error.rb
92
+ - lib/gourami/extensions/changes.rb
90
93
  - lib/gourami/extensions/resources.rb
91
94
  - lib/gourami/form.rb
92
95
  - lib/gourami/formatting_constants.rb
96
+ - lib/gourami/not_watching_changes_error.rb
93
97
  - lib/gourami/required_attribute_error.rb
94
98
  - lib/gourami/validation_error.rb
95
99
  - lib/gourami/validations.rb
96
100
  - lib/gourami/version.rb
101
+ - lib/gourami/watch_changes_error.rb
97
102
  homepage: http://github.com/Vydia/gourami
98
103
  licenses:
99
104
  - MIT
@@ -114,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
119
  version: '0'
115
120
  requirements: []
116
121
  rubyforge_project:
117
- rubygems_version: 2.4.8
122
+ rubygems_version: 2.4.6
118
123
  signing_key:
119
124
  specification_version: 4
120
125
  summary: Keep your Routes, Controllers and Models thin.