turbo_boost-commands 0.2.2 → 0.3.1

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