unpoly-rails 2.7.2.2 → 3.0.0.rc1

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.
@@ -9,11 +9,23 @@ module Unpoly
9
9
 
10
10
  # TODO: Docs
11
11
  def clear(pattern = '*')
12
- change.clear_cache = pattern
12
+ ActiveSupport::Deprecation.warn("up.cache.clear is deprecated. Use up.cache.expire instead.")
13
+ expire(pattern)
14
+ end
15
+
16
+ # TODO: Docs
17
+ def expire(pattern = '*')
18
+ change.expire_cache = pattern
19
+ end
20
+
21
+ # TODO: Docs
22
+ def evict(pattern = '*')
23
+ change.evict_cache = pattern
13
24
  end
14
25
 
15
26
  def keep
16
- clear(false)
27
+ ActiveSupport::Deprecation.warn("up.cache.keep is deprecated. Use up.cache.expire(false) instead.")
28
+ expire(false)
17
29
  end
18
30
 
19
31
  private
@@ -31,6 +31,8 @@ module Unpoly
31
31
  raise NotImplementedError
32
32
  end
33
33
 
34
+ ##
35
+ # A string value, serialized as itself.
34
36
  class String < Field
35
37
 
36
38
  def parse(raw)
@@ -38,46 +40,71 @@ module Unpoly
38
40
  end
39
41
 
40
42
  def stringify(value)
41
- value.to_s
43
+ unless value.nil?
44
+ value.to_s
45
+ end
42
46
  end
43
47
 
44
48
  end
45
49
 
46
- class Boolean < Field
50
+ ##
51
+ # An array of strings, serialized as JSON.
52
+ class SeparatedValues < Field
53
+
54
+ def initialize(name, separator: ' ', default: nil)
55
+ super(name)
56
+ @separator = separator
57
+ @default = default
58
+ end
47
59
 
48
60
  def parse(raw)
49
- raw == 'true'
61
+ if raw
62
+ raw.split(@separator)
63
+ elsif @default
64
+ instance_exec(&@default)
65
+ end
50
66
  end
51
67
 
52
68
  def stringify(value)
53
- Util.safe_json_encode(value)
69
+ unless value.nil?
70
+ value.join(@separator)
71
+ end
54
72
  end
55
73
 
56
74
  end
57
75
 
76
+ ##
77
+ # A date and time value, serialized as the number of seconds since the epoch.
58
78
  class Time < Field
59
79
 
60
80
  def parse(raw)
61
81
  if raw.present?
62
82
  ::Time.at(raw.to_i)
63
83
  end
64
- end
84
+ end
65
85
 
66
86
  def stringify(value)
67
- if value
87
+ unless value.nil?
68
88
  value.to_i
69
89
  end
70
90
  end
71
91
 
72
92
  end
73
93
 
94
+ ##
95
+ # A hash of values, serialized as JSON.
74
96
  class Hash < Field
75
97
 
98
+ def initialize(name, default: nil)
99
+ super(name)
100
+ @default = default
101
+ end
102
+
76
103
  def parse(raw)
77
104
  if raw.present?
78
- result = Util.json_decode(raw)
79
- else
80
- result = {}
105
+ result = ActiveSupport::JSON.decode(raw)
106
+ elsif @default
107
+ result = instance_exec(&@default)
81
108
  end
82
109
 
83
110
  if result.is_a?(::Hash)
@@ -88,25 +115,36 @@ module Unpoly
88
115
  end
89
116
 
90
117
  def stringify(value)
91
- Util.safe_json_encode(value)
118
+ unless value.nil?
119
+ ActiveSupport::JSON.encode(value)
120
+ end
92
121
  end
93
122
 
94
123
  end
95
124
 
125
+ ##
126
+ # An array of values, serialized as JSON.
96
127
  class Array < Field
97
128
 
129
+ def initialize(name, default: nil)
130
+ super(name)
131
+ @default = default
132
+ end
133
+
98
134
  def parse(raw)
99
135
  if raw.present?
100
- result = Util.json_decode(raw)
101
- else
102
- result = []
136
+ result = ActiveSupport::JSON.decode(raw)
137
+ elsif @default
138
+ result = instance_exec(&@default)
103
139
  end
104
140
 
105
141
  result
106
142
  end
107
143
 
108
144
  def stringify(value)
109
- Util.safe_json_encode(value)
145
+ unless value.nil?
146
+ ActiveSupport::JSON.encode(value)
147
+ end
110
148
  end
111
149
 
112
150
  end
@@ -10,6 +10,26 @@ module Unpoly
10
10
  # Rails 3.2 delegate generated invalid Ruby with `to: :class`.
11
11
  delegate :fields, to: :get_class
12
12
 
13
+ def vary?
14
+ if @vary.nil?
15
+ @vary = true
16
+ end
17
+
18
+ @vary
19
+ end
20
+
21
+ def vary=(vary)
22
+ @vary = vary
23
+ end
24
+
25
+ def no_vary(&block)
26
+ previous_vary = vary?
27
+ self.vary = false
28
+ block.call
29
+ ensure
30
+ self.vary = previous_vary
31
+ end
32
+
13
33
  private
14
34
 
15
35
  def get_class
@@ -18,15 +38,32 @@ module Unpoly
18
38
 
19
39
  module ClassMethods
20
40
 
21
- def field(name, type, method: name, response_header_name: nil)
22
- field = type.new(name)
41
+ def field(field, method: nil, response_header_name: nil, request_header_name: nil)
42
+ method ||= field.name
23
43
 
24
44
  define_method "#{method}_field" do
25
45
  field
26
46
  end
27
47
 
48
+ define_method "#{method}_request_header_name" do
49
+ request_header_name || field.header_name
50
+ end
51
+
52
+ define_method "#{method}_request_header_accessed!" do
53
+ return unless vary?
54
+ header_name = send("#{method}_request_header_name")
55
+ earlier_varies = response.headers['Vary']&.split(/\s*,\s*/) || []
56
+ response.headers['Vary'] = (earlier_varies | [header_name]).join(', ')
57
+ end
58
+
59
+ define_method "#{method}_response_header_name" do
60
+ response_header_name || field.header_name
61
+ end
62
+
28
63
  define_method "#{method}_from_request_headers" do
29
- raw_value = request.headers[field.header_name]
64
+ header_name = send("#{method}_request_header_name")
65
+ raw_value = request.headers[header_name]
66
+ send("#{method}_request_header_accessed!")
30
67
  field.parse(raw_value)
31
68
  end
32
69
 
@@ -61,10 +98,11 @@ module Unpoly
61
98
  value = send(method)
62
99
  stringified = field.stringify(value)
63
100
  if stringified.present? # app servers don't like blank header values
64
- header_name = response_header_name || field.header_name
101
+ header_name = send("#{method}_response_header_name" )
65
102
  response.headers[header_name] = stringified
66
103
  end
67
104
  end
105
+
68
106
  end
69
107
 
70
108
  end
@@ -40,14 +40,14 @@ module Unpoly
40
40
  # TODO: Docs
41
41
  def accept(value = nil)
42
42
  overlay? or raise CannotClose, 'Cannot accept the root layer'
43
- change.response.headers['X-Up-Accept-Layer'] = Util.safe_json_encode(value)
43
+ change.response.headers['X-Up-Accept-Layer'] = value.to_json
44
44
  end
45
45
 
46
46
  ##
47
47
  # TODO: Docs
48
48
  def dismiss(value = nil)
49
49
  overlay? or raise CannotClose, 'Cannot dismiss the root layer'
50
- change.response.headers['X-Up-Dismiss-Layer'] = Util.safe_json_encode(value)
50
+ change.response.headers['X-Up-Dismiss-Layer'] = value.to_json
51
51
  end
52
52
 
53
53
  private
@@ -57,4 +57,4 @@ module Unpoly
57
57
  end
58
58
  end
59
59
  end
60
- end
60
+ end
@@ -14,18 +14,19 @@ module Unpoly
14
14
  end
15
15
 
16
16
  # Generate helpers to get, set and cast fields in request and response headers.
17
- field :version, Field::String
18
- field :target, Field::String
19
- field :fail_target, Field::String
20
- field :validate, Field::String
21
- field :mode, Field::String
22
- field :fail_mode, Field::String
23
- field :context, Field::Hash, method: :input_context
24
- field :fail_context, Field::Hash, method: :input_fail_context
25
- field :context_changes, Field::Hash, response_header_name: 'X-Up-Context'
26
- field :events, Field::Array
27
- field :clear_cache, Field::String
28
- field :reload_from_time, Field::Time
17
+ field Field::String.new(:version)
18
+ field Field::String.new(:target)
19
+ field Field::String.new(:fail_target)
20
+ field Field::SeparatedValues.new(:validate_names), request_header_name: 'X-Up-Validate'
21
+ field Field::String.new(:mode)
22
+ field Field::String.new(:fail_mode)
23
+ field Field::Hash.new(:context, default: -> { {} }), method: :input_context
24
+ field Field::Hash.new(:fail_context, default: -> { {} }), method: :input_fail_context
25
+ field Field::Hash.new(:context_changes, default: -> { {} }), response_header_name: 'X-Up-Context'
26
+ field Field::Array.new(:events, default: -> { [] })
27
+ field Field::String.new(:expire_cache)
28
+ field Field::String.new(:evict_cache)
29
+ field Field::Time.new(:reload_from_time)
29
30
 
30
31
  ##
31
32
  # Returns whether the current request is an
@@ -65,7 +66,10 @@ module Unpoly
65
66
  end
66
67
 
67
68
  def target_changed?
68
- target != target_from_request
69
+ # The target has changed if either:
70
+ # (1) The #target= setter was called, setting @server_target
71
+ # (2) An up[target] param was set to preserve a previously changed target through a redirect.
72
+ (@server_target && @server_target != target_from_request_headers) || target_from_params
69
73
  end
70
74
 
71
75
  ##
@@ -82,8 +86,10 @@ module Unpoly
82
86
  test_target(target, tested_target)
83
87
  end
84
88
 
85
- def render_nothing(options = {})
86
- status = options.fetch(:status, :ok)
89
+ def render_nothing(status: :no_content, deprecation: true)
90
+ if deprecation
91
+ ActiveSupport::Deprecation.warn("up.render_nothing is deprecated. Use head(:no_content) instead.")
92
+ end
87
93
  self.target = ':none'
88
94
  controller.head(status)
89
95
  end
@@ -132,23 +138,29 @@ module Unpoly
132
138
  end
133
139
 
134
140
  ##
135
- # Returns whether the current form submission should be
136
- # [validated](https://unpoly.com/input-up-validate) (and not be saved to the database).
137
- def validate?
138
- validate.present?
141
+ # If the current form submission is a [validation](https://unpoly.com/input-up-validate),
142
+ # this returns the name attributes of the form fields that has triggered
143
+ # the validation.
144
+ #
145
+ # Note that multiple validating form fields may be batched into a single request.
146
+ def validate_names
147
+ validate_names_from_request
139
148
  end
140
149
 
141
- alias validating? validate?
150
+ memoize def validate_name
151
+ if validating?
152
+ validates_names.first
153
+ end
154
+ end
142
155
 
143
156
  ##
144
- # If the current form submission is a [validation](https://unpoly.com/input-up-validate),
145
- # this returns the name attribute of the form field that has triggered
146
- # the validation.
147
- memoize def validate
148
- validate_from_request
157
+ # Returns whether the current form submission should be
158
+ # [validated](https://unpoly.com/input-up-validate) (and not be saved to the database).
159
+ def validate?
160
+ validate_names.present?
149
161
  end
150
162
 
151
- alias validate_name validate
163
+ alias validating? validate?
152
164
 
153
165
  ##
154
166
  # TODO: Docs
@@ -235,19 +247,22 @@ module Unpoly
235
247
  end
236
248
 
237
249
  def after_action
238
- write_events_to_response_headers
250
+ no_vary do
251
+ write_events_to_response_headers
239
252
 
240
- write_clear_cache_to_response_headers
253
+ write_expire_cache_to_response_headers
254
+ write_evict_cache_to_response_headers
241
255
 
242
- if context_changes.present?
243
- write_context_changes_to_response_headers
244
- end
256
+ if context_changes.present?
257
+ write_context_changes_to_response_headers
258
+ end
245
259
 
246
- if target_changed?
247
- # Only write the target to the response if it has changed.
248
- # The client might have a more abstract target like :main
249
- # that we don't want to override with an echo of the first match.
250
- write_target_to_response_headers
260
+ if target_changed?
261
+ # Only write the target to the response if it has changed.
262
+ # The client might have a more abstract target like :main
263
+ # that we don't want to override with an echo of the first match.
264
+ write_target_to_response_headers
265
+ end
251
266
  end
252
267
  end
253
268
 
@@ -291,26 +306,46 @@ module Unpoly
291
306
  Cache.new(self)
292
307
  end
293
308
 
294
- def clear_cache
309
+ def expire_cache
295
310
  # Cache commands are outgoing only. They wouldn't be passed as a request header.
296
311
  # We might however pass them as params so they can survive a redirect.
297
- if @clear_cache.nil?
298
- clear_cache_from_params
312
+ if @expire_cache.nil?
313
+ expire_cache_from_params
299
314
  else
300
- @clear_cache
315
+ @expire_cache
301
316
  end
302
317
  end
303
318
 
304
- def clear_cache=(value)
305
- @clear_cache = value
319
+ def expire_cache=(value)
320
+ @expire_cache = value
306
321
  end
307
322
 
308
- def reload_from_time
309
- reload_from_time_from_request
323
+ def evict_cache
324
+ # Cache commands are outgoing only. They wouldn't be passed as a request header.
325
+ # We might however pass them as params so they can survive a redirect.
326
+ if @evict_cache.nil?
327
+ evict_cache_from_params
328
+ else
329
+ @evict_cache
330
+ end
310
331
  end
311
332
 
312
- def reload?
313
- !!reload_from_time
333
+ def evict_cache=(value)
334
+ @evict_cache = value
335
+ end
336
+
337
+ def reload_from_time(deprecation: true)
338
+ if deprecation
339
+ ActiveSupport::Deprecation.warn("up.reload_from_time is deprecated. Use conditional GETs instead: https://guides.rubyonrails.org/caching_with_rails.html#conditional-get-support")
340
+ end
341
+ reload_from_time_from_request || if_modified_since
342
+ end
343
+
344
+ def reload?(deprecation: true)
345
+ if deprecation
346
+ ActiveSupport::Deprecation.warn("up.reload? is deprecated. Use conditional GETs instead: https://guides.rubyonrails.org/caching_with_rails.html#conditional-get-support")
347
+ end
348
+ !!reload_from_time(deprecation: false)
314
349
  end
315
350
 
316
351
  def safe_callback(code)
@@ -328,6 +363,12 @@ module Unpoly
328
363
 
329
364
  delegate :request, :params, :response, to: :controller
330
365
 
366
+ def if_modified_since
367
+ if (header = request.headers['If-Modified-Since'])
368
+ Time.httpdate(header)
369
+ end
370
+ end
371
+
331
372
  def content_security_policy_nonce
332
373
  controller.send(:content_security_policy_nonce)
333
374
  end
@@ -356,18 +397,25 @@ module Unpoly
356
397
 
357
398
  def fields_as_params
358
399
  params = {}
359
- params[version_param_name] = serialized_version
360
- params[target_param_name] = serialized_target
361
- params[fail_target_param_name] = serialized_fail_target
362
- params[validate_param_name] = serialized_validate
363
- params[mode_param_name] = serialized_mode
364
- params[fail_mode_param_name] = serialized_fail_mode
365
- params[input_context_param_name] = serialized_input_context
366
- params[input_fail_context_param_name] = serialized_input_fail_context
367
- params[context_changes_param_name] = serialized_context_changes
368
- params[events_param_name] = serialized_events
369
- params[clear_cache_param_name] = serialized_clear_cache
370
- params[reload_from_time_param_name] = serialized_reload_from_time
400
+
401
+ # When the browser sees a redirect it will automatically resend the request headers
402
+ # from the original request. This means that headers we set on the client (X-Up-Target, X-Up-Mode, etc.)
403
+ # are automatically preserved through redirects.
404
+ #
405
+ # We still need to handle all the response headers that we set on the server.
406
+ # There are encoded as params.
407
+ params[context_changes_param_name] = serialized_context_changes # server-set header must be sent if present
408
+ params[events_param_name] = serialized_events # server-set header must be sent if present
409
+ params[expire_cache_param_name] = serialized_expire_cache # server-set header must be sent if present
410
+ params[evict_cache_param_name] = serialized_evict_cache # server-set header must be sent if present
411
+
412
+ if target_changed?
413
+ # Only write the target to the response if it has changed.
414
+ # The client might have a more abstract target like :main
415
+ # that we don't want to override with an echo of the first match.
416
+ params[target_param_name] = serialized_target
417
+ end
418
+
371
419
 
372
420
  # Don't send empty response headers.
373
421
  params = params.select { |_key, value| value.present? }
@@ -33,11 +33,13 @@ module Unpoly
33
33
  ##
34
34
  # TODO: Docs
35
35
  def redirect_to(target, *args)
36
- if up?
37
- target = url_for(target)
38
- target = up.url_with_field_values(target)
36
+ up.no_vary do
37
+ if up?
38
+ target = url_for(target)
39
+ target = up.url_with_field_values(target)
40
+ end
41
+ super(target, *args)
39
42
  end
40
- super(target, *args)
41
43
  end
42
44
 
43
45
  ::ActionController::Base.prepend(self)
@@ -12,7 +12,7 @@ module Unpoly
12
12
  spec_folder = root.join('spec')
13
13
 
14
14
  # If a local application has referenced the local gem sources
15
- # (e.g. `gem 'unpoly', path: '../unpoly'`) we use the local build.
15
+ # (e.g. `gem 'unpoly-rails', path: '../unpoly-rails'`) we use the local build.
16
16
  # This way changes from the Webpack watcher are immediately picked
17
17
  # up by the application.
18
18
  is_local_gem = spec_folder.directory?
@@ -18,13 +18,9 @@ module Unpoly
18
18
  end
19
19
 
20
20
  private
21
-
21
+
22
22
  def set_up_request_echo_headers
23
- request_url_without_up_params = up.request_url_without_up_params
24
- unless request_url_without_up_params == request.original_url
25
- response.headers['X-Up-Location'] = up.request_url_without_up_params
26
- end
27
-
23
+ response.headers['X-Up-Location'] = up.request_url_without_up_params
28
24
  response.headers['X-Up-Method'] = request.method
29
25
  end
30
26
 
@@ -4,6 +4,6 @@ module Unpoly
4
4
  # The current version of the unpoly-rails gem.
5
5
  # This version number is also used for releases of the Unpoly
6
6
  # frontend code.
7
- VERSION = '2.7.2.2'
7
+ VERSION = '3.0.0.rc1'
8
8
  end
9
9
  end
data/lib/unpoly-rails.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'memoized'
2
2
  require_relative 'unpoly/rails/version'
3
3
  require_relative 'unpoly/rails/error'
4
- require_relative 'unpoly/rails/util'
5
4
  require_relative 'unpoly/rails/engine'
6
5
  require_relative 'unpoly/rails/request_echo_headers'
7
6
  require_relative 'unpoly/rails/request_method_cookie'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unpoly-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.2.2
4
+ version: 3.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-30 00:00:00.000000000 Z
11
+ date: 2023-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -119,8 +119,6 @@ files:
119
119
  - assets/unpoly/unpoly-migrate.js
120
120
  - assets/unpoly/unpoly-migrate.min.js
121
121
  - assets/unpoly/unpoly.css
122
- - assets/unpoly/unpoly.es5.js
123
- - assets/unpoly/unpoly.es5.min.js
124
122
  - assets/unpoly/unpoly.es6.js
125
123
  - assets/unpoly/unpoly.es6.min.js
126
124
  - assets/unpoly/unpoly.js
@@ -138,7 +136,6 @@ files:
138
136
  - lib/unpoly/rails/error.rb
139
137
  - lib/unpoly/rails/request_echo_headers.rb
140
138
  - lib/unpoly/rails/request_method_cookie.rb
141
- - lib/unpoly/rails/util.rb
142
139
  - lib/unpoly/rails/version.rb
143
140
  homepage: https://github.com/unpoly/unpoly-rails
144
141
  licenses:
@@ -155,9 +152,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
155
152
  version: 2.3.0
156
153
  required_rubygems_version: !ruby/object:Gem::Requirement
157
154
  requirements:
158
- - - ">="
155
+ - - ">"
159
156
  - !ruby/object:Gem::Version
160
- version: '0'
157
+ version: 1.3.1
161
158
  requirements: []
162
159
  rubygems_version: 3.2.16
163
160
  signing_key: