turbo_boost-commands 0.2.2 → 0.3.1

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +122 -37
  3. data/app/assets/builds/@turbo-boost/commands.js +1 -1
  4. data/app/assets/builds/@turbo-boost/commands.js.map +4 -4
  5. data/app/assets/builds/@turbo-boost/commands.metafile.json +1 -1
  6. data/app/controllers/concerns/turbo_boost/commands/controller.rb +1 -1
  7. data/app/javascript/drivers/index.js +1 -1
  8. data/app/javascript/elements.js +0 -1
  9. data/app/javascript/events.js +6 -3
  10. data/app/javascript/headers.js +2 -2
  11. data/app/javascript/index.js +20 -11
  12. data/app/javascript/invoker.js +2 -10
  13. data/app/javascript/lifecycle.js +3 -6
  14. data/app/javascript/logger.js +29 -2
  15. data/app/javascript/renderer.js +11 -5
  16. data/app/javascript/schema.js +2 -1
  17. data/app/javascript/state/index.js +50 -33
  18. data/app/javascript/state/observable.js +1 -1
  19. data/app/javascript/state/page.js +34 -0
  20. data/app/javascript/state/storage.js +11 -0
  21. data/app/javascript/turbo.js +0 -10
  22. data/app/javascript/version.js +1 -1
  23. data/lib/turbo_boost/commands/attribute_set.rb +8 -0
  24. data/lib/turbo_boost/commands/command.rb +8 -3
  25. data/lib/turbo_boost/commands/command_callbacks.rb +23 -6
  26. data/lib/turbo_boost/commands/command_validator.rb +44 -0
  27. data/lib/turbo_boost/commands/controller_pack.rb +10 -10
  28. data/lib/turbo_boost/commands/engine.rb +14 -10
  29. data/lib/turbo_boost/commands/errors.rb +15 -8
  30. data/lib/turbo_boost/commands/{middleware.rb → middlewares/entry_middleware.rb} +30 -21
  31. data/lib/turbo_boost/commands/middlewares/exit_middleware.rb +63 -0
  32. data/lib/turbo_boost/commands/patches/action_view_helpers_tag_helper_tag_builder_patch.rb +10 -2
  33. data/lib/turbo_boost/commands/responder.rb +28 -0
  34. data/lib/turbo_boost/commands/runner.rb +150 -186
  35. data/lib/turbo_boost/commands/sanitizer.rb +1 -1
  36. data/lib/turbo_boost/commands/state.rb +97 -47
  37. data/lib/turbo_boost/commands/state_store.rb +72 -0
  38. data/lib/turbo_boost/commands/token_validator.rb +51 -0
  39. data/lib/turbo_boost/commands/version.rb +1 -1
  40. metadata +29 -8
@@ -1,16 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboBoost::Commands
4
- class InvalidTokenError < StandardError; end
5
-
6
- class InvalidClassError < StandardError; end
7
-
8
- class InvalidMethodError < StandardError; end
9
-
10
- class InvalidElementError < StandardError; end
4
+ class StateError < StandardError
5
+ end
11
6
 
12
7
  class CommandError < StandardError
13
- def initialize(*messages, command:, http_status:, cause: nil)
8
+ def initialize(*messages, command:, http_status: :internal_server_error, cause: nil)
14
9
  @cause = cause
15
10
  @command = command
16
11
  @http_status_code = TurboBoost::Commands.http_status_code(http_status)
@@ -30,6 +25,18 @@ module TurboBoost::Commands
30
25
  end
31
26
  end
32
27
 
28
+ class InvalidTokenError < CommandError
29
+ end
30
+
31
+ class InvalidClassError < CommandError
32
+ end
33
+
34
+ class InvalidMethodError < CommandError
35
+ end
36
+
37
+ class InvalidElementError < CommandError
38
+ end
39
+
33
40
  class AbortError < CommandError
34
41
  def initialize(*messages, **kwargs)
35
42
  messages.prepend "TurboBoost Command intentionally aborted via `throw` in a `[before,after,around]_command` lifecycle callback!"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class TurboBoost::Commands::Middleware
3
+ class TurboBoost::Commands::EntryMiddleware
4
4
  PATH = "/turbo-boost-command-invocation"
5
5
 
6
6
  def initialize(app)
@@ -35,6 +35,11 @@ class TurboBoost::Commands::Middleware
35
35
  false
36
36
  end
37
37
 
38
+ def convert_to_get_request?(driver)
39
+ return true if driver == "frame" || driver == "window"
40
+ false
41
+ end
42
+
38
43
  # Modifies the given POST request so Rails sees it as GET.
39
44
  #
40
45
  # The posted JSON body content holds the TurboBoost Command meta data.
@@ -42,19 +47,21 @@ class TurboBoost::Commands::Middleware
42
47
  #
43
48
  # @example POST payload for: /turbo-boost-command-invocation
44
49
  # {
45
- # "id" => "turbo-command-f824ded1-a86e-4a36-9442-ea2165a64569", # unique command invocation id
46
- # "name" => "IncrementCountCommand", # the command being invoked
47
- # "elementId" => nil, # the triggering element's dom id
48
- # "elementAttributes" => {"tag"=>"BUTTON", "checked"=>false, "disabled"=>false, "value"=>nil}, # the triggering element's attributes
49
- # "startedAt" => 1708213193567, # the time the command was invoked
50
- # "changedState" => {}, # the delta of optimistic state changes made on the client
51
- # "clientState" => { # the state as it was on the client
52
- # "command_token" => "IlU0dVVhNElFdkVCZVUi--a878d33d85ed9b9611c155ed1d7bb8785fb..."} # the command token used for forgery protection
50
+ # "csrfToken" => "..." # Rails' CSRF token
51
+ # "id" => "turbo-command-f824ded1-a86e-4a36-9442-ea2165a64569", # Unique ID for the command invocation
52
+ # "name" => "ExampleCommand#perform", # Name of command being invoked
53
+ # "elementId" => nil, # Triggering element's DOM id
54
+ # "elementAttributes" => {...}, # Triggering element's attributes
55
+ # "startedAt" => 1708213193567, # Time the command was invoked
56
+ # "elementCache" => {...}, # Cache of ALL tracked element attributes (optimistic changes)
57
+ # "state" => { # State ... TODO: HOPSOFT
58
+ # "page" => {...}, # - transient page state (element attributes, etc.)
59
+ # "signed" => "", # - signed state used for the last server render (untampered)
60
+ # "unsigned" => {...} # - state with optimistic changes from the client
53
61
  # },
54
- # "signedState" => "eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL2R1VuaXZlcnNhbElEOjpFeH...", # the state as it was on the server at the time of the last command invocation
55
- # "driver" => "frame", # the driver used to invoke the command
56
- # "frameId" => "basic_command-turbo-frame", # the turbo-frame id (if applicable)
57
- # "src" => "/basic_command.turbo_stream" # the URL to present to Rails (turbo-frame src, window location, etc.)
62
+ # "driver" => "frame", # Driver used to invoke the command
63
+ # "frameId" => "...", # TurboFrame id (if applicable)
64
+ # "src" => "..." # URL to present to Rails (turbo-frame src, window location, etc.)
58
65
  # }
59
66
  #
60
67
  # @param request [Rack::Request] the request to modify
@@ -64,20 +71,22 @@ class TurboBoost::Commands::Middleware
64
71
 
65
72
  request.env.tap do |env|
66
73
  # Store the command params in the environment
67
- env["turbo_boost_command"] = params
68
-
69
- # Change the method from POST to GET
70
- env["REQUEST_METHOD"] = "GET"
74
+ env["turbo_boost_command_params"] = params
71
75
 
72
76
  # Update the URI, PATH_INFO, and QUERY_STRING
73
77
  env["REQUEST_URI"] = uri.to_s if env.key?("REQUEST_URI")
74
78
  env["PATH_INFO"] = uri.path
75
79
  env["QUERY_STRING"] = uri.query.to_s
76
80
 
77
- # Clear the body and related headers so the appears and behaves like a GET
78
- env["rack.input"] = StringIO.new
79
- env["CONTENT_LENGTH"] = "0"
80
- env.delete("CONTENT_TYPE")
81
+ # Change the method from POST to GET
82
+ if convert_to_get_request?(params["driver"])
83
+ env["REQUEST_METHOD"] = "GET"
84
+
85
+ # Clear the body and related headers so the appears and behaves like a GET
86
+ env["rack.input"] = StringIO.new
87
+ env["CONTENT_LENGTH"] = "0"
88
+ env.delete("CONTENT_TYPE")
89
+ end
81
90
  end
82
91
  rescue => error
83
92
  puts "#{self.class.name} failed to modify the request! #{error.message}"
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TurboBoost::Commands::ExitMiddleware
4
+ BODY_PATTERN = /<\/\s*body/io
5
+ TURBO_FRAME_PATTERN = /<\/\s*turbo-frame/io
6
+ TURBO_STREAM_PATTERN = /<\/\s*turbo-stream/io
7
+ TAIL_PATTERN = /\z/io
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ @params = env["turbo_boost_command_params"]
15
+ response = @app.call(env)
16
+ return modify!(env, response) if modify?(env)
17
+ response
18
+ end
19
+
20
+ private
21
+
22
+ def modify?(env)
23
+ env["turbo_boost_command_responder"].is_a? TurboBoost::Commands::Responder
24
+ end
25
+
26
+ def modify!(env, response)
27
+ responder = env["turbo_boost_command_responder"]
28
+ status, headers, body = response
29
+ new_body = body_to_a(body).join
30
+
31
+ case response_type(new_body)
32
+ when :body
33
+ match = new_body.match(BODY_PATTERN).to_s
34
+ new_body.sub! BODY_PATTERN, [responder.body, match].join
35
+ when :frame
36
+ match = new_body.match(TURBO_FRAME_PATTERN).to_s
37
+ new_body.sub! TURBO_FRAME_PATTERN, [responder.body, match].join
38
+ else
39
+ new_body << responder.body
40
+ end
41
+
42
+ [status, headers.merge(responder.headers), [new_body]]
43
+ rescue => error
44
+ Rails.logger.error "TurboBoost::Commands::Runner failed to append to the response! #{error.message}"
45
+ [status, headers, body]
46
+ end
47
+
48
+ def body_to_a(body)
49
+ return [] if body.nil?
50
+ return body if body.is_a?(Array)
51
+ return [body] if body.is_a?(String)
52
+ return body.to_ary if body.respond_to?(:to_ary)
53
+ return body.each.to_a if body.respond_to?(:each)
54
+ [body.to_s]
55
+ end
56
+
57
+ def response_type(body)
58
+ return :body if body.match?(BODY_PATTERN)
59
+ return :frame if body.match?(TURBO_FRAME_PATTERN)
60
+ return :stream if body.match?(TURBO_STREAM_PATTERN)
61
+ :unknown
62
+ end
63
+ end
@@ -4,7 +4,15 @@ require_relative "../attribute_hydration"
4
4
 
5
5
  module TurboBoost::Commands::Patches::ActionViewHelpersTagHelperTagBuilderPatch
6
6
  def tag_options(options, ...)
7
- dehydrated_options = TurboBoost::Commands::AttributeHydration.dehydrate(options)
8
- super(dehydrated_options, ...)
7
+ options = turbo_boost&.state&.tag_options(options) || options
8
+ options = TurboBoost::Commands::AttributeHydration.dehydrate(options)
9
+ super(options, ...)
10
+ end
11
+
12
+ private
13
+
14
+ def turbo_boost
15
+ return nil unless @view_context.respond_to?(:turbo_boost)
16
+ @view_context.turbo_boost
9
17
  end
10
18
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TurboBoost::Commands::Responder
4
+ attr_accessor :headers
5
+
6
+ def initialize
7
+ @headers = HashWithIndifferentAccess.new
8
+ @body = Set.new
9
+ end
10
+
11
+ def body
12
+ @body.join.html_safe
13
+ end
14
+
15
+ def add_header(key, value)
16
+ headers[key.to_s.downcase] = value.to_s
17
+ end
18
+
19
+ def add_content(content)
20
+ @body << sanitizer.sanitize(content) + "\n"
21
+ end
22
+
23
+ private
24
+
25
+ def sanitizer
26
+ TurboBoost::Commands::Sanitizer.instance
27
+ end
28
+ end