unpoly-rails 2.7.1 → 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
- value.to_json
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
105
  result = ActiveSupport::JSON.decode(raw)
79
- else
80
- result = {}
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
- ActiveSupport::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
136
  result = ActiveSupport::JSON.decode(raw)
101
- else
102
- result = []
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
- ActiveSupport::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
@@ -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
 
@@ -265,14 +280,18 @@ module Unpoly
265
280
  # Parse the URL to extract the ?query part below.
266
281
  uri = URI.parse(original_url)
267
282
 
268
- # This parses the query as a flat list of key/value pairs.
269
- params = Rack::Utils.parse_query(uri.query)
283
+ # Split at &
284
+ query_parts = uri.query.split('&')
270
285
 
271
286
  # We only used the up[...] params to transport headers, but we don't
272
287
  # want them to appear in a history URL.
273
- non_up_params = params.reject { |key, _value| key.starts_with?(Field::PARAM_PREFIX) }
288
+ non_up_query_parts = query_parts.reject { |query_part| query_part.start_with?(Field::PARAM_PREFIX) }
274
289
 
275
- append_params_to_url(uri.path, non_up_params)
290
+ if non_up_query_parts.empty?
291
+ uri.path
292
+ else
293
+ "#{uri.path}?#{non_up_query_parts.join('&')}"
294
+ end
276
295
  end
277
296
 
278
297
  memoize def layer
@@ -287,26 +306,46 @@ module Unpoly
287
306
  Cache.new(self)
288
307
  end
289
308
 
290
- def clear_cache
309
+ def expire_cache
310
+ # Cache commands are outgoing only. They wouldn't be passed as a request header.
311
+ # We might however pass them as params so they can survive a redirect.
312
+ if @expire_cache.nil?
313
+ expire_cache_from_params
314
+ else
315
+ @expire_cache
316
+ end
317
+ end
318
+
319
+ def expire_cache=(value)
320
+ @expire_cache = value
321
+ end
322
+
323
+ def evict_cache
291
324
  # Cache commands are outgoing only. They wouldn't be passed as a request header.
292
325
  # We might however pass them as params so they can survive a redirect.
293
- if @clear_cache.nil?
294
- clear_cache_from_params
326
+ if @evict_cache.nil?
327
+ evict_cache_from_params
295
328
  else
296
- @clear_cache
329
+ @evict_cache
297
330
  end
298
331
  end
299
332
 
300
- def clear_cache=(value)
301
- @clear_cache = value
333
+ def evict_cache=(value)
334
+ @evict_cache = value
302
335
  end
303
336
 
304
- def reload_from_time
305
- reload_from_time_from_request
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
306
342
  end
307
343
 
308
- def reload?
309
- !!reload_from_time
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)
310
349
  end
311
350
 
312
351
  def safe_callback(code)
@@ -324,6 +363,12 @@ module Unpoly
324
363
 
325
364
  delegate :request, :params, :response, to: :controller
326
365
 
366
+ def if_modified_since
367
+ if (header = request.headers['If-Modified-Since'])
368
+ Time.httpdate(header)
369
+ end
370
+ end
371
+
327
372
  def content_security_policy_nonce
328
373
  controller.send(:content_security_policy_nonce)
329
374
  end
@@ -352,18 +397,25 @@ module Unpoly
352
397
 
353
398
  def fields_as_params
354
399
  params = {}
355
- params[version_param_name] = serialized_version
356
- params[target_param_name] = serialized_target
357
- params[fail_target_param_name] = serialized_fail_target
358
- params[validate_param_name] = serialized_validate
359
- params[mode_param_name] = serialized_mode
360
- params[fail_mode_param_name] = serialized_fail_mode
361
- params[input_context_param_name] = serialized_input_context
362
- params[input_fail_context_param_name] = serialized_input_fail_context
363
- params[context_changes_param_name] = serialized_context_changes
364
- params[events_param_name] = serialized_events
365
- params[clear_cache_param_name] = serialized_clear_cache
366
- 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
+
367
419
 
368
420
  # Don't send empty response headers.
369
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?
@@ -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.1'
7
+ VERSION = '3.0.0.rc1'
8
8
  end
9
9
  end
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.1
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: 2022-11-10 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,8 @@ 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
122
+ - assets/unpoly/unpoly.es6.js
123
+ - assets/unpoly/unpoly.es6.min.js
124
124
  - assets/unpoly/unpoly.js
125
125
  - assets/unpoly/unpoly.min.css
126
126
  - assets/unpoly/unpoly.min.js
@@ -152,9 +152,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
152
152
  version: 2.3.0
153
153
  required_rubygems_version: !ruby/object:Gem::Requirement
154
154
  requirements:
155
- - - ">="
155
+ - - ">"
156
156
  - !ruby/object:Gem::Version
157
- version: '0'
157
+ version: 1.3.1
158
158
  requirements: []
159
159
  rubygems_version: 3.2.16
160
160
  signing_key: