unpoly-rails 2.7.2.2 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: