turbo_boost-commands 0.2.2 → 0.3.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 +4 -4
- data/README.md +96 -29
- data/app/assets/builds/@turbo-boost/commands.js +1 -1
- data/app/assets/builds/@turbo-boost/commands.js.map +4 -4
- data/app/assets/builds/@turbo-boost/commands.metafile.json +1 -1
- data/app/controllers/concerns/turbo_boost/commands/controller.rb +1 -1
- data/app/javascript/elements.js +0 -1
- data/app/javascript/events.js +6 -3
- data/app/javascript/headers.js +2 -2
- data/app/javascript/index.js +20 -11
- data/app/javascript/invoker.js +2 -10
- data/app/javascript/lifecycle.js +3 -6
- data/app/javascript/logger.js +29 -2
- data/app/javascript/renderer.js +11 -5
- data/app/javascript/schema.js +2 -1
- data/app/javascript/state/index.js +47 -34
- data/app/javascript/state/observable.js +1 -1
- data/app/javascript/state/page.js +33 -0
- data/app/javascript/state/storage.js +11 -0
- data/app/javascript/turbo.js +0 -10
- data/app/javascript/version.js +1 -1
- data/lib/turbo_boost/commands/attribute_set.rb +8 -0
- data/lib/turbo_boost/commands/command.rb +8 -3
- data/lib/turbo_boost/commands/command_callbacks.rb +23 -6
- data/lib/turbo_boost/commands/command_validator.rb +44 -0
- data/lib/turbo_boost/commands/controller_pack.rb +10 -10
- data/lib/turbo_boost/commands/engine.rb +14 -10
- data/lib/turbo_boost/commands/errors.rb +15 -8
- data/lib/turbo_boost/commands/{middleware.rb → middlewares/entry_middleware.rb} +30 -21
- data/lib/turbo_boost/commands/middlewares/exit_middleware.rb +63 -0
- data/lib/turbo_boost/commands/patches/action_view_helpers_tag_helper_tag_builder_patch.rb +10 -2
- data/lib/turbo_boost/commands/responder.rb +28 -0
- data/lib/turbo_boost/commands/runner.rb +150 -186
- data/lib/turbo_boost/commands/sanitizer.rb +1 -1
- data/lib/turbo_boost/commands/state.rb +97 -47
- data/lib/turbo_boost/commands/state_store.rb +72 -0
- data/lib/turbo_boost/commands/token_validator.rb +51 -0
- data/lib/turbo_boost/commands/version.rb +1 -1
- metadata +29 -8
@@ -1,73 +1,123 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "digest/md5"
|
4
|
+
require_relative "state_store"
|
5
|
+
|
6
|
+
# Class that encapsulates all the various forms of state.
|
7
|
+
#
|
8
|
+
# 1. `page` - Client-side transient page state used for rendering remembered element attributes
|
9
|
+
# 2. `now` - Server-side state for the current render only (discarded after rendering)
|
10
|
+
# 3. `signed` - Server-side state that persists across renders (state that was used for the last server-side render)
|
11
|
+
# 4. `unsigned` - Client-side state (optimistic client-side changes)
|
12
|
+
# 5. `current` - Combined server-side state (signed + now)
|
13
|
+
# 6. `all` - All state except unsigned (signed + now + page)
|
14
|
+
#
|
3
15
|
class TurboBoost::Commands::State
|
4
16
|
include Enumerable
|
5
17
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
end
|
18
|
+
def initialize(payload = {})
|
19
|
+
payload = payload.respond_to?(:to_unsafe_h) ? payload.to_unsafe_h : payload.to_h
|
20
|
+
payload = payload.with_indifferent_access
|
11
21
|
|
12
|
-
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
22
|
+
@now = {}.with_indifferent_access
|
23
|
+
@page = payload.fetch(:page, {}).with_indifferent_access
|
24
|
+
@signed = TurboBoost::Commands::StateStore.new(payload.fetch(:signed, {}))
|
25
|
+
@unsigned = payload.fetch(:unsigned, {}).with_indifferent_access
|
16
26
|
end
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
28
|
+
# Client-side transient page state used for rendering remembered element attributes
|
29
|
+
# @return [HashWithIndifferentAccess]
|
30
|
+
attr_reader :page
|
31
|
+
|
32
|
+
# Server-side state for the current render only (similar to flash.now)
|
33
|
+
# @note Discarded after rendering
|
34
|
+
# @return [HashWithIndifferentAccess]
|
35
|
+
attr_reader :now
|
36
|
+
|
37
|
+
# Server-side state that persists across renders
|
38
|
+
# This is the state that was used for the last server-side render (untampered by the client)
|
39
|
+
# @return [TurboBoost::Commands::StateStore]
|
40
|
+
attr_reader :signed
|
41
|
+
|
42
|
+
# @note Most state will interactions work with the signed state, so we delegate missing methods to it.
|
43
|
+
delegate_missing_to :signed
|
44
|
+
|
45
|
+
# Client-side state (optimistic client-side changes)
|
46
|
+
# @note There is a hook on Command instances to resolve state `Command#resolve_state`,
|
47
|
+
# where Command authors can determine how to properly handle optimistic client-side state.
|
48
|
+
# @return [HashWithIndifferentAccess]
|
49
|
+
attr_reader :unsigned
|
50
|
+
alias_method :optimistic, :unsigned
|
51
|
+
|
52
|
+
# Combined server-side state (signed + now)
|
53
|
+
# @return [HashWithIndifferentAccess]
|
54
|
+
def current
|
55
|
+
signed.to_h.merge now
|
23
56
|
end
|
24
57
|
|
25
|
-
|
26
|
-
hash.to_h.each { |key, val| self[key] = val }
|
27
|
-
self
|
28
|
-
end
|
58
|
+
delegate :each, to: :current
|
29
59
|
|
30
|
-
|
31
|
-
|
60
|
+
# All state except unsigned (page + current).
|
61
|
+
# @return [HashWithIndifferentAccess]
|
62
|
+
def all
|
63
|
+
page.merge current
|
32
64
|
end
|
33
65
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
!!@provisional
|
66
|
+
# Returns a cache key representing "all" state
|
67
|
+
def cache_key
|
68
|
+
"TurboBoost::Commands::State/#{Digest::MD5.base64digest(all.to_s)}"
|
38
69
|
end
|
39
70
|
|
40
|
-
|
41
|
-
|
42
|
-
|
71
|
+
# A JSON representation of state that can be sent to the client
|
72
|
+
#
|
73
|
+
# Includes the following keys:
|
74
|
+
# * `signed` - The signed state (String)
|
75
|
+
# * `unsigned` - The unsigned state (Hash)
|
76
|
+
#
|
77
|
+
# @return [String]
|
78
|
+
def to_json
|
79
|
+
{signed: signed.to_sgid_param, unsigned: signed.to_h}.to_json(camelize: false)
|
43
80
|
end
|
44
81
|
|
45
|
-
def
|
46
|
-
|
47
|
-
end
|
82
|
+
def tag_options(options = {})
|
83
|
+
return options unless options.is_a?(Hash)
|
48
84
|
|
49
|
-
|
50
|
-
|
51
|
-
end
|
85
|
+
options = options.deep_symbolize_keys
|
86
|
+
return options unless options.key?(:turbo_boost)
|
52
87
|
|
53
|
-
|
54
|
-
|
55
|
-
end
|
88
|
+
config = options.delete(:turbo_boost)
|
89
|
+
return options unless config.is_a?(Hash)
|
56
90
|
|
57
|
-
|
58
|
-
|
59
|
-
end
|
91
|
+
attributes = config[:remember]
|
92
|
+
return options if attributes.blank?
|
60
93
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
94
|
+
attributes = begin
|
95
|
+
attributes.is_a?(Array) ? attributes : JSON.parse(attributes.to_s)
|
96
|
+
rescue
|
97
|
+
raise TurboBoost::Commands::StateError,
|
98
|
+
"Invalid `turbo_boost` options! `attributes` must be an Array of attributes to remember!"
|
99
|
+
end
|
100
|
+
attributes ||= []
|
101
|
+
attributes.map!(&:to_s).uniq!
|
102
|
+
return options if attributes.blank?
|
65
103
|
|
66
|
-
|
104
|
+
if options[:id].blank?
|
105
|
+
raise TurboBoost::Commands::StateError, "An `id` attribute is required for remembering state!"
|
106
|
+
end
|
67
107
|
|
68
|
-
|
108
|
+
options[:aria] ||= {}
|
109
|
+
options[:data] ||= {}
|
110
|
+
options[:data][:turbo_boost_state_attributes] = attributes.to_json
|
111
|
+
|
112
|
+
attributes.each do |name|
|
113
|
+
value = page.dig(options[:id], name)
|
114
|
+
case name
|
115
|
+
in String if name.start_with?("aria-") then options[:aria][name.delete_prefix("aria-").to_sym] = value
|
116
|
+
in String if name.start_with?("data-") then options[:data][name.delete_prefix("data-").to_sym] = value
|
117
|
+
else options[name.to_sym] = value
|
118
|
+
end
|
119
|
+
end
|
69
120
|
|
70
|
-
|
71
|
-
store.instance_variable_get(:@data) || {}
|
121
|
+
options
|
72
122
|
end
|
73
123
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TurboBoost::Commands::StateStore < ActiveSupport::Cache::MemoryStore
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
class NoCoder
|
7
|
+
def dump(value)
|
8
|
+
value
|
9
|
+
end
|
10
|
+
|
11
|
+
def load(value)
|
12
|
+
value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
SGID_PURPOSE = name.dup.freeze
|
17
|
+
|
18
|
+
def initialize(payload = {})
|
19
|
+
super(coder: NoCoder.new, compress: false, expires_in: 1.day, size: 16.kilobytes)
|
20
|
+
|
21
|
+
begin
|
22
|
+
payload = case payload
|
23
|
+
when SignedGlobalID then URI::UID.from_sgid(payload, for: SGID_PURPOSE)&.decode
|
24
|
+
when GlobalID then URI::UID.from_gid(payload)&.decode
|
25
|
+
when String then URI::UID.from_sgid(payload, for: SGID_PURPOSE)&.decode || URI::UID.from_gid(payload, for: SGID_PURPOSE)&.decode
|
26
|
+
else payload
|
27
|
+
end
|
28
|
+
rescue => error
|
29
|
+
Rails.logger.error "Failed to decode URI::UID when creating a TurboBoost::Commands::StateStore! #{error.message}"
|
30
|
+
payload = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
merge! payload
|
34
|
+
end
|
35
|
+
|
36
|
+
alias_method :[], :read
|
37
|
+
alias_method :[]=, :write
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
@data
|
41
|
+
.each_with_object({}) { |(key, entry), memo| memo[key] = entry.value }
|
42
|
+
.with_indifferent_access
|
43
|
+
end
|
44
|
+
|
45
|
+
delegate :dig, :each, to: :to_h
|
46
|
+
|
47
|
+
def merge!(other = {})
|
48
|
+
other.to_h.each { |key, val| write key, val }
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_uid
|
53
|
+
cleanup
|
54
|
+
URI::UID.build to_h, include_blank: false
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_gid
|
58
|
+
to_uid.to_gid
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_gid_param
|
62
|
+
to_gid.to_param
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_sgid
|
66
|
+
to_uid.to_sgid for: SGID_PURPOSE, expires_in: 1.day
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_sgid_param
|
70
|
+
to_sgid.to_param
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TurboBoost::Commands::TokenValidator
|
4
|
+
def initialize(command, method_name)
|
5
|
+
@command = command
|
6
|
+
@method_name = method_name
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :command, :method_name
|
10
|
+
delegate :controller, to: :command
|
11
|
+
delegate :session, to: :controller
|
12
|
+
|
13
|
+
def validate
|
14
|
+
return true unless TurboBoost::Commands.config.protect_from_forgery
|
15
|
+
tokens.any? { |token| valid_token? token }
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :valid?, :validate
|
19
|
+
|
20
|
+
def validate!
|
21
|
+
return true if valid?
|
22
|
+
|
23
|
+
message = <<~MSG
|
24
|
+
`#{command.class.name}##{method_name}` invoked with an invalid authenticity token!
|
25
|
+
|
26
|
+
Verify that your page includes `<%= csrf_meta_tags %>` in the header.
|
27
|
+
|
28
|
+
If the problem persists, you can disable forgery protection with `TurboBoost::Commands.config.protect_from_forgery = false`
|
29
|
+
MSG
|
30
|
+
|
31
|
+
raise TurboBoost::Commands::InvalidTokenError.new(message, command: command.class)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def tokens
|
37
|
+
list = Set.new.tap do |set|
|
38
|
+
set.add command.params[:csrf_token]
|
39
|
+
|
40
|
+
# TODO: Update to use Rails' public API
|
41
|
+
set.merge controller.send(:request_authenticity_tokens)
|
42
|
+
end
|
43
|
+
|
44
|
+
list.select(&:present?).to_a
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid_token?(token)
|
48
|
+
# TODO: Update to use Rails' public API
|
49
|
+
controller.send :valid_authenticity_token?, session, token
|
50
|
+
end
|
51
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbo_boost-commands
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Hopkins (hopsoft)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.1.7
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: amazing_print
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: capybara
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -336,16 +350,16 @@ dependencies:
|
|
336
350
|
name: sqlite3
|
337
351
|
requirement: !ruby/object:Gem::Requirement
|
338
352
|
requirements:
|
339
|
-
- - "
|
353
|
+
- - "~>"
|
340
354
|
- !ruby/object:Gem::Version
|
341
|
-
version: '
|
355
|
+
version: '1.7'
|
342
356
|
type: :development
|
343
357
|
prerelease: false
|
344
358
|
version_requirements: !ruby/object:Gem::Requirement
|
345
359
|
requirements:
|
346
|
-
- - "
|
360
|
+
- - "~>"
|
347
361
|
- !ruby/object:Gem::Version
|
348
|
-
version: '
|
362
|
+
version: '1.7'
|
349
363
|
- !ruby/object:Gem::Dependency
|
350
364
|
name: standardrb
|
351
365
|
requirement: !ruby/object:Gem::Requirement
|
@@ -431,6 +445,8 @@ files:
|
|
431
445
|
- app/javascript/schema.js
|
432
446
|
- app/javascript/state/index.js
|
433
447
|
- app/javascript/state/observable.js
|
448
|
+
- app/javascript/state/page.js
|
449
|
+
- app/javascript/state/storage.js
|
434
450
|
- app/javascript/turbo.js
|
435
451
|
- app/javascript/urls.js
|
436
452
|
- app/javascript/uuids.js
|
@@ -440,16 +456,21 @@ files:
|
|
440
456
|
- lib/turbo_boost/commands/attribute_set.rb
|
441
457
|
- lib/turbo_boost/commands/command.rb
|
442
458
|
- lib/turbo_boost/commands/command_callbacks.rb
|
459
|
+
- lib/turbo_boost/commands/command_validator.rb
|
443
460
|
- lib/turbo_boost/commands/controller_pack.rb
|
444
461
|
- lib/turbo_boost/commands/engine.rb
|
445
462
|
- lib/turbo_boost/commands/errors.rb
|
446
463
|
- lib/turbo_boost/commands/http_status_codes.rb
|
447
|
-
- lib/turbo_boost/commands/
|
464
|
+
- lib/turbo_boost/commands/middlewares/entry_middleware.rb
|
465
|
+
- lib/turbo_boost/commands/middlewares/exit_middleware.rb
|
448
466
|
- lib/turbo_boost/commands/patches.rb
|
449
467
|
- lib/turbo_boost/commands/patches/action_view_helpers_tag_helper_tag_builder_patch.rb
|
468
|
+
- lib/turbo_boost/commands/responder.rb
|
450
469
|
- lib/turbo_boost/commands/runner.rb
|
451
470
|
- lib/turbo_boost/commands/sanitizer.rb
|
452
471
|
- lib/turbo_boost/commands/state.rb
|
472
|
+
- lib/turbo_boost/commands/state_store.rb
|
473
|
+
- lib/turbo_boost/commands/token_validator.rb
|
453
474
|
- lib/turbo_boost/commands/version.rb
|
454
475
|
homepage: https://github.com/hopsoft/turbo_boost-commands
|
455
476
|
licenses:
|
@@ -473,7 +494,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
473
494
|
- !ruby/object:Gem::Version
|
474
495
|
version: '0'
|
475
496
|
requirements: []
|
476
|
-
rubygems_version: 3.5.
|
497
|
+
rubygems_version: 3.5.10
|
477
498
|
signing_key:
|
478
499
|
specification_version: 4
|
479
500
|
summary: Commands to help you build robust reactive applications with Rails & Hotwire.
|