rails_redhot 0.1.1 → 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
  SHA256:
3
- metadata.gz: 2bc4f84383d654009f5141fe5f0b2a7133d92a9cfa7cb8b95be9076430964928
4
- data.tar.gz: e41024b7b92269e0d683d639928ce295f17ffff8f43541c6af803d1c96964353
3
+ metadata.gz: 8f5c72d2cc5029c74afb124b5bbc728d007f4d635ad9569781cdd5b2fa3f6e25
4
+ data.tar.gz: eaca540b27f70101cefaee8d29e78ab843c12d873ee89bc382b28a0af963ee28
5
5
  SHA512:
6
- metadata.gz: edbc120531cccaafad095636979183cf818505f7213a4bdc2957f47dc298905c039bbf9897efbf8ae23ba3bb055ae46c0c1132b5f78805a1fb4aa658ff0fc8d7
7
- data.tar.gz: 58bab798b14f920c6e037d54afdc57b02ec44000dafacad5adab5c9c10c0bb792d17a340391f31157d793323d8c1e9742960fbf3c0b610e2b46f1cf8a5d68d96
6
+ metadata.gz: 3029222021c8639cba0a7d60e9684e4571bbcd391c1a8884fdf2386b0a0c0b6b28afb4e85816a9f6f6f95d50a80825de754e2dc74939ca4511f0c6eb6c57fa3b
7
+ data.tar.gz: 6a27cf8634d4ccb20e833a79352ab810d2fe674c2e077a2a9c672d993657d7eb90a9ce2af7bbbd6e72981f03f11c22b4953c92e636bea25c6c2cabe1f8b70940
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.2.0 - March 14, 2023
2
+
3
+ - Demo application: Use Ruby 3.2.0, Rails 7.0.4.2
4
+ - Added simplecov and improve testcoverage
5
+ - Added reducer errors (ActiveModel::Errors), separate from the models own error object
6
+ - Use deep_dup before dispatching an action to make sure the original action is never
7
+ modified by reducer methods. Use deep_symbolize_keys on an action for convenience
8
+ - Added after_change callback
9
+
1
10
  # 0.1.1 - April 14, 2022
2
11
 
3
12
  - Fix calling flatten! multiple times erased initial state
@@ -16,4 +25,4 @@
16
25
 
17
26
  # 0.0.1 - November 16, 2021
18
27
 
19
- - Initial release, not pulished to rubygems
28
+ - Initial release, not published to rubygems
data/README.md CHANGED
@@ -109,15 +109,17 @@ class Foobar < ApplicationRecord
109
109
  private
110
110
 
111
111
  def my_redux_reducers
112
- ->(state, action) {
113
- case action[:type]
114
- when :add
115
- state[:total] += 1
116
- when :remove
117
- state[:total] -= 1
118
- end
119
- state
120
- },
112
+ [
113
+ ->(state, action) {
114
+ case action[:type]
115
+ when :add
116
+ state[:total] += 1
117
+ when :remove
118
+ state[:total] -= 1
119
+ end
120
+ state
121
+ }
122
+ ]
121
123
  end
122
124
  end
123
125
  ```
@@ -128,7 +130,7 @@ Or specify your own reducer method:
128
130
  acts_as_redux :my_redux, reducers: :my_list_of_reducers
129
131
 
130
132
  def my_list_of_reducers
131
- # ...
133
+ # ...
132
134
  ```
133
135
 
134
136
  ### Undo/redo
@@ -148,7 +150,8 @@ In the controller action use the `undo!` method to perform the action.
148
150
  For redoing actions the similar methods `redo?`, `redo_action` and `redo!` are available.
149
151
 
150
152
  ### Flatten
151
- You can 'save' the current state. Essentially this flattens the list of actions to the initial state.
153
+ You can 'save' the current state. Essentially this copies the current view state to the initial state
154
+ and truncates the list of actions. Redo and undo are not possible until new actions are added.
152
155
  Methods `flatten?` and `flatten!` can be used in a view and controller:
153
156
 
154
157
  ```ruby
@@ -192,11 +195,86 @@ on the reducer functions you have implemented.
192
195
  For a full working example see the demo applications [view](test/dummy/app/views/foobars/_editor.html.erb)
193
196
  and [controller](test/dummy/app/controllers/foobars_controller.rb).
194
197
 
198
+ ### Adding errors
199
+ Just like you can add validation errors on a model, you can add errors inside your reducer methods.
200
+ There is an ActiveModel::Errors object for redux errors. It is separate from the one for the model and
201
+ can be accessed via `reduce_errors`. Use it like this:
202
+
203
+ ```ruby
204
+ def my_redux_reducers
205
+ @my_redux_reducers ||= [
206
+ -> (state, action) {
207
+ case action[:type]&.to_sym
208
+ when :add
209
+ if action[:item].length <= 6
210
+ state[:items] << { id: next_seq_id, value: CGI.escape(action[:item]) }
211
+ else
212
+ reduce_errors.add(:item, :too_long, { count: 6 })
213
+ end
214
+ end
215
+
216
+ state
217
+ }
218
+ ]
219
+ end
220
+ ```
221
+
222
+ Since ActiveModel does not know anything about attributes living inside your redux store
223
+ using `reduce_errors.full_messages` won't work. You can create you own error to message translation or
224
+ supply `:base` as the attribute name plus a message.
225
+ See the [Rails documentation](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add).
226
+
227
+ ```ruby
228
+ reduce_errors.add(:base, message: 'Item should have between 1 and 6 characters')
229
+ ```
230
+
231
+ If any reduce error is present the `dispatch!` method will return false.
232
+ To check if there are any errors present (to prevent saving the model) use:
233
+
234
+ ```ruby
235
+ @foobar.reduce_valid?
236
+ ```
237
+
238
+ In the controller you may want to reload the model if the dispatch action gave an error,
239
+ so the old state is rendered.
240
+
241
+ ### After change callback
242
+ Sometimes you want to do something after the redux store has changed.
243
+ For instance to manipulate the view state based on all entries
244
+ (the reducer methods only handle one action at the time).
245
+ This callback method is called after `dispatch!`, `undo!` and `redo!`.
246
+ In the model:
247
+
248
+ ```ruby
249
+ class AnotherFooBar < ApplicationRecord
250
+ include RailsRedhot::ActsAsRedux
251
+
252
+ acts_as_redux :another_redux_store, after_change: :my_after_change_actions
253
+
254
+ private
255
+
256
+ def another_redux_store_reducers
257
+ [
258
+ -> (state, _action) {
259
+ state[:items] ||= []
260
+ state
261
+ },
262
+ # ...
263
+ ]
264
+ end
265
+
266
+ def my_after_change_actions
267
+ # Do something with the view_state
268
+ # view_state[:items].each { do_something }
269
+ end
270
+ end
271
+ ```
272
+
195
273
  ## Security
196
- Care must be taken to not introduce any vulnerabilities.
274
+ Care must be taken to not introduce any vulnerabilities!
197
275
  When passing values from the request to the reducer functions treat any string or complex
198
- values as potential candidates for SQL injection. Either sanitize the value or `CGI.escape`
199
- a string before adding it to the redux store.
276
+ values as potential candidates for SQL injection. Either sanitize or `CGI.escape`
277
+ strings before adding them to the redux store.
200
278
 
201
279
  ## Installation
202
280
  Add this line to your application's Gemfile:
@@ -242,10 +320,9 @@ The gem is available as open source under the terms of the [MIT License](https:/
242
320
 
243
321
  ## Remarks
244
322
 
245
- - Developed using Ruby 3.0.3
246
323
  - This gem is not designed to handle very large lists of actions and state.
247
- When calling `undo` the state is rebuilt from scratch
248
- If the list of actions to process is large this would become slow.
324
+ When calling `undo` the state is rebuilt from scratch,
325
+ if the list of actions to process is large this would become slow.
249
326
  One would need add 'savepoints' that regularly save the state and rebuild
250
327
  the current state from that point forward
251
328
  - Stricly speaking, hotwire is not needed for this gem to work. Just using
@@ -8,6 +8,8 @@ module RailsRedhot
8
8
  class_methods do
9
9
  def acts_as_redux(store_name, options = {})
10
10
  reducers = options.key?(:reducers) ? options[:reducers] : "#{store_name}_reducers".to_sym
11
+ reducer_errors = nil
12
+ reducer_after_change = options[:after_change]
11
13
 
12
14
  store(store_name, accessors: [ :initial_state, :state, :actions, :head, :seq_id ], coder: JSON)
13
15
 
@@ -43,8 +45,7 @@ module RailsRedhot
43
45
  self.state = initial_state
44
46
  if head > -1
45
47
  actions[0..head].each { |action| perform_reduce(action) }
46
- else
47
- perform_reduce(initial_state)
48
+ self.send(reducer_after_change) if reducer_after_change
48
49
  end
49
50
  true
50
51
  else
@@ -56,6 +57,7 @@ module RailsRedhot
56
57
  if redo?
57
58
  self.head += 1
58
59
  perform_reduce(actions[head])
60
+ self.send(reducer_after_change) if reducer_after_change
59
61
  true
60
62
  else
61
63
  false
@@ -70,6 +72,14 @@ module RailsRedhot
70
72
  true
71
73
  end
72
74
 
75
+ define_method('reduce_errors') do
76
+ reducer_errors
77
+ end
78
+
79
+ define_method('reduce_valid?') do
80
+ reducer_errors.details.empty?
81
+ end
82
+
73
83
  define_method('next_seq_id') do
74
84
  self.seq_id += 1
75
85
  end
@@ -80,19 +90,29 @@ module RailsRedhot
80
90
 
81
91
  self.actions << action
82
92
  self.head += 1
83
- perform_reduce(action)
84
- true
93
+ perform_reduce(action.deep_dup.deep_symbolize_keys)
94
+ self.send(reducer_after_change) if reducer_after_change
95
+ reduce_valid?
85
96
  end
86
97
 
87
98
  # private
88
99
 
100
+ define_method('reset_reduce_errors') do
101
+ reducer_errors = ActiveModel::Errors.new(self)
102
+ end
103
+
89
104
  define_method('load_store') do
90
105
  self.initial_state ||= {}
91
106
  # self.state is initially nil: no need to store state twice when there are no actions
92
107
  self.head ||= -1
93
108
  self.actions ||= []
94
109
  self.seq_id ||= 0
95
- perform_reduce({}) if state.blank? && initial_state.blank?
110
+
111
+ if state.blank? && initial_state.blank?
112
+ perform_reduce({})
113
+ else
114
+ reset_reduce_errors
115
+ end
96
116
  end
97
117
 
98
118
  define_method('all_reducers') do
@@ -100,10 +120,12 @@ module RailsRedhot
100
120
  end
101
121
 
102
122
  define_method('perform_reduce') do |action|
123
+ reset_reduce_errors
124
+
103
125
  self.state = all_reducers.reduce(
104
- view_state.dup.deep_symbolize_keys
126
+ view_state.deep_dup.deep_symbolize_keys
105
127
  ) do |current_state, reducer|
106
- reducer.call(current_state, action.deep_symbolize_keys)
128
+ reducer.call(current_state, action)
107
129
  end
108
130
  end
109
131
 
@@ -1,4 +1,4 @@
1
1
  module RailsRedhot
2
2
  # Gem version
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_redhot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivo Herweijer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-14 00:00:00.000000000 Z
11
+ date: 2023-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -61,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
61
  - !ruby/object:Gem::Version
62
62
  version: '0'
63
63
  requirements: []
64
- rubygems_version: 3.3.7
64
+ rubygems_version: 3.4.6
65
65
  signing_key:
66
66
  specification_version: 4
67
67
  summary: REDux pattern for HOTwire == Redhot