turbo_boost-commands 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -29
  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/elements.js +0 -1
  8. data/app/javascript/events.js +6 -3
  9. data/app/javascript/headers.js +2 -2
  10. data/app/javascript/index.js +20 -11
  11. data/app/javascript/invoker.js +2 -10
  12. data/app/javascript/lifecycle.js +3 -6
  13. data/app/javascript/logger.js +29 -2
  14. data/app/javascript/renderer.js +11 -5
  15. data/app/javascript/schema.js +2 -1
  16. data/app/javascript/state/index.js +47 -34
  17. data/app/javascript/state/observable.js +1 -1
  18. data/app/javascript/state/page.js +33 -0
  19. data/app/javascript/state/storage.js +11 -0
  20. data/app/javascript/turbo.js +0 -10
  21. data/app/javascript/version.js +1 -1
  22. data/lib/turbo_boost/commands/attribute_set.rb +8 -0
  23. data/lib/turbo_boost/commands/command.rb +8 -3
  24. data/lib/turbo_boost/commands/command_callbacks.rb +23 -6
  25. data/lib/turbo_boost/commands/command_validator.rb +44 -0
  26. data/lib/turbo_boost/commands/controller_pack.rb +10 -10
  27. data/lib/turbo_boost/commands/engine.rb +14 -10
  28. data/lib/turbo_boost/commands/errors.rb +15 -8
  29. data/lib/turbo_boost/commands/{middleware.rb → middlewares/entry_middleware.rb} +30 -21
  30. data/lib/turbo_boost/commands/middlewares/exit_middleware.rb +63 -0
  31. data/lib/turbo_boost/commands/patches/action_view_helpers_tag_helper_tag_builder_patch.rb +10 -2
  32. data/lib/turbo_boost/commands/responder.rb +28 -0
  33. data/lib/turbo_boost/commands/runner.rb +150 -186
  34. data/lib/turbo_boost/commands/sanitizer.rb +1 -1
  35. data/lib/turbo_boost/commands/state.rb +97 -47
  36. data/lib/turbo_boost/commands/state_store.rb +72 -0
  37. data/lib/turbo_boost/commands/token_validator.rb +51 -0
  38. data/lib/turbo_boost/commands/version.rb +1 -1
  39. metadata +29 -8
@@ -6,7 +6,7 @@ import confirmation from './confirmation'
6
6
  import delegates from './delegates'
7
7
  import drivers from './drivers'
8
8
  import elements from './elements'
9
- import lifecycle from './lifecycle'
9
+ import './lifecycle'
10
10
  import logger from './logger'
11
11
  import state from './state'
12
12
  import uuids from './uuids'
@@ -29,14 +29,17 @@ const Commands = {
29
29
 
30
30
  function buildCommandPayload(id, element) {
31
31
  return {
32
- id, // uniquely identifies the command
33
- name: element.getAttribute(schema.commandAttribute),
34
- elementId: element.id.length > 0 ? element.id : null,
35
- elementAttributes: elements.buildAttributePayload(element),
36
- startedAt: Date.now(),
37
- changedState: state.changed, // changed-state (delta of optimistic updates)
38
- clientState: state.current, // client-side state
39
- signedState: state.signed // server-side state
32
+ csrfToken: document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'), // -- Rails CSRF token
33
+ id, //-------------------------------------------------------------------------------------- Uniquely identifies the command invocation
34
+ name: element.getAttribute(schema.commandAttribute), //------------------------------------- Command name
35
+ elementId: element.id.length > 0 ? element.id : null, //------------------------------------ ID of the element that triggered the command
36
+ elementAttributes: elements.buildAttributePayload(element), //------------------------------ Attributes of the element that triggered the command
37
+ startedAt: Date.now(), //------------------------------------------------------------------- Start time of when the command was invoked
38
+ state: {
39
+ page: state.buildPageState(),
40
+ signed: state.signed,
41
+ unsigned: state.unsigned
42
+ }
40
43
  }
41
44
  }
42
45
 
@@ -49,7 +52,7 @@ async function invokeCommand(event) {
49
52
  if (!element) return
50
53
  if (!delegates.isRegisteredForElement(event.type, element)) return
51
54
 
52
- const commandId = `turbo-command-${uuids.v4()}`
55
+ const commandId = uuids.v4()
53
56
  let driver = drivers.find(element)
54
57
  let payload = {
55
58
  ...buildCommandPayload(commandId, element),
@@ -108,6 +111,7 @@ if (!self.TurboBoost.Commands) {
108
111
  delegates.handler = invokeCommand
109
112
  delegates.register('click', [`[${schema.commandAttribute}]`])
110
113
  delegates.register('submit', [`form[${schema.commandAttribute}]`])
114
+ delegates.register('toggle', [`details[${schema.commandAttribute}]`])
111
115
  delegates.register('change', [
112
116
  `input[${schema.commandAttribute}]`,
113
117
  `select[${schema.commandAttribute}]`,
@@ -115,7 +119,12 @@ if (!self.TurboBoost.Commands) {
115
119
  ])
116
120
 
117
121
  self.TurboBoost.Commands = Commands
118
- self.TurboBoost.State = state
122
+ self.TurboBoost.State = {
123
+ initialize: state.initialize,
124
+ get current() {
125
+ return state.unsigned
126
+ }
127
+ }
119
128
  }
120
129
 
121
130
  export default Commands
@@ -1,24 +1,16 @@
1
1
  import headers from './headers'
2
2
  import lifecycle from './lifecycle'
3
- import state from './state'
4
3
  import urls from './urls'
5
4
  import { dispatch } from './events'
6
5
  import { render } from './renderer'
7
6
 
8
7
  const parseError = error => {
9
- const errorMessage = `Unexpected error performing a TurboBoost Command! ${error.message}`
10
- dispatch(lifecycle.events.clientError, document, { detail: { error: errorMessage } }, true)
8
+ const message = `Unexpected error performing a TurboBoost Command! ${error.message}`
9
+ dispatch(lifecycle.events.clientError, document, { detail: { message, error } }, true)
11
10
  }
12
11
 
13
12
  const parseAndRenderResponse = response => {
14
13
  const { strategy } = headers.tokenize(response.headers.get(headers.RESPONSE_HEADER))
15
-
16
- // FAIL: Status outside the range of 200-399
17
- if (response.status < 200 || response.status > 399) {
18
- const error = `Server returned a ${response.status} status code! TurboBoost Commands require 2XX-3XX status codes.`
19
- dispatch(lifecycle.events.serverError, document, { detail: { error, response } }, true)
20
- }
21
-
22
14
  response.text().then(content => render(strategy, content))
23
15
  }
24
16
 
@@ -2,14 +2,11 @@ import activity from './activity'
2
2
  import { dispatch, commandEvents } from './events'
3
3
 
4
4
  function finish(event) {
5
- event.detail.endedAt = Date.now()
6
- event.detail.milliseconds = event.detail.endedAt - event.detail.startedAt
7
- setTimeout(() => dispatch(commandEvents.finish, event.target, { detail: event.detail }), 25)
5
+ setTimeout(() => dispatch(commandEvents.finish, event.target, { detail: event.detail }))
8
6
  }
9
7
 
10
- // TODO: forward source event to finish (error or success)
11
- addEventListener(commandEvents.serverError, finish)
12
- addEventListener(commandEvents.success, finish)
8
+ const events = [commandEvents.abort, commandEvents.serverError, commandEvents.success]
9
+ events.forEach(name => addEventListener(name, finish))
13
10
  addEventListener(commandEvents.finish, event => activity.remove(event.detail.id), true)
14
11
 
15
12
  export default { events: commandEvents }
@@ -1,4 +1,5 @@
1
- import { allEvents as events } from './events'
1
+ // TODO: Move Logger to its own library (i.e. TurboBoost.Logger)
2
+ import { commandEvents as events } from './events'
2
3
 
3
4
  let currentLevel = 'unknown'
4
5
  let initialized = false
@@ -28,10 +29,36 @@ const shouldLogEvent = event => {
28
29
  return true
29
30
  }
30
31
 
32
+ const logMethod = event => {
33
+ if (logLevels.error.includes(event.type)) return 'error'
34
+ if (logLevels.warn.includes(event.type)) return 'warn'
35
+ if (logLevels.info.includes(event.type)) return 'info'
36
+ if (logLevels.debug.includes(event.type)) return 'debug'
37
+ return 'log'
38
+ }
39
+
31
40
  const logEvent = event => {
32
41
  if (shouldLogEvent(event)) {
33
42
  const { target, type, detail } = event
34
- console[currentLevel](type, detail.id || '', { target, detail })
43
+ const id = detail.id || ''
44
+ const commandName = detail.name || ''
45
+
46
+ let duration = ''
47
+ if (detail.startedAt) duration = `${Date.now() - detail.startedAt}ms `
48
+
49
+ const typeParts = type.split(':')
50
+ const lastPart = typeParts.pop()
51
+ const eventName = `%c${typeParts.join(':')}:%c${lastPart}`
52
+ const message = [`%c${commandName}`, `%c${duration}`, eventName]
53
+
54
+ console[logMethod(event)](
55
+ message.join(' ').replace(/\s{2,}/g, ' '),
56
+ 'color:deepskyblue',
57
+ 'color:lime',
58
+ 'color:darkgray',
59
+ eventName.match(/abort|error/i) ? 'color:red' : 'color:deepskyblue',
60
+ { id, detail, target }
61
+ )
35
62
  }
36
63
  }
37
64
 
@@ -1,5 +1,3 @@
1
- import uuids from './uuids'
2
-
3
1
  const append = content => {
4
2
  document.body.insertAdjacentHTML('beforeend', content)
5
3
  }
@@ -7,12 +5,20 @@ const append = content => {
7
5
  const replace = content => {
8
6
  const parser = new DOMParser()
9
7
  const doc = parser.parseFromString(content, 'text/html')
10
- TurboBoost.Streams.morph.method(document.documentElement, doc.documentElement)
8
+ const head = document.querySelector('head')
9
+ const body = document.querySelector('body')
10
+ const newHead = doc.querySelector('head')
11
+ const newBody = doc.querySelector('body')
12
+ if (head && newHead) TurboBoost?.Streams?.morph?.method(head, newHead)
13
+ if (body && newBody) TurboBoost?.Streams?.morph?.method(body, newBody)
11
14
  }
12
15
 
16
+ // TODO: dispatch events after append/replace so we can apply page state
13
17
  export const render = (strategy, content) => {
14
- if (strategy.match(/^Append$/i)) return append(content)
15
- if (strategy.match(/^Replace$/i)) return replace(content)
18
+ if (strategy && content) {
19
+ if (strategy.match(/^Append$/i)) return append(content)
20
+ if (strategy.match(/^Replace$/i)) return replace(content)
21
+ }
16
22
  }
17
23
 
18
24
  export default { render }
@@ -3,7 +3,8 @@ const schema = {
3
3
  frameAttribute: 'data-turbo-frame',
4
4
  methodAttribute: 'data-turbo-method',
5
5
  commandAttribute: 'data-turbo-command',
6
- confirmAttribute: 'data-turbo-confirm'
6
+ confirmAttribute: 'data-turbo-confirm',
7
+ stateAttributesAttribute: 'data-turbo-boost-state-attributes'
7
8
  }
8
9
 
9
10
  export default { ...schema }
@@ -1,44 +1,57 @@
1
- // TODO: Consider moving State to its own library
1
+ // TODO: Move State to its own library
2
2
  import observable from './observable'
3
- import { dispatch, commandEvents, stateEvents } from '../events'
4
-
5
- let initialState, currentState, changedState, signedState
6
-
7
- function initialize(initial, signed) {
8
- const json = JSON.parse(initial)
9
- initialState = { ...json }
10
- signedState = signed
11
- currentState = observable(json)
12
- changedState = {}
13
- setTimeout(() =>
14
- dispatch(stateEvents.stateLoad, document, {
15
- detail: { state: currentState }
16
- })
17
- )
3
+ import page from './page'
4
+ import storage from './storage'
5
+ import { dispatch, stateEvents } from '../events'
6
+
7
+ const key = 'TurboBoost::State'
8
+ const stub = { pages: {}, signed: null, unsigned: {} }
9
+
10
+ let signed = null // signed state <string>
11
+ let unsigned = {} // unsigned state (optimistic) <object>
12
+
13
+ const restore = () => {
14
+ const saved = { ...stub, ...storage.find(key) }
15
+ signed = saved.signed
16
+ unsigned = observable(saved.unsigned)
17
+ saved.pages[location.pathname] = saved.pages[location.pathname] || {}
18
+ page.restoreState(saved.pages[location.pathname])
18
19
  }
19
20
 
20
- addEventListener(stateEvents.stateChange, event => {
21
- for (const [key, value] of Object.entries(currentState))
22
- if (initialState[key] !== value) changedState[key] = value
23
- })
24
-
25
- export default {
26
- initialize,
27
- events: stateEvents,
21
+ const save = () => {
22
+ const saved = { ...stub, ...storage.find(key) }
23
+ const fresh = {
24
+ signed: signed || saved.signed,
25
+ unsigned: { ...saved.unsigned, ...unsigned },
26
+ pages: { ...saved.pages }
27
+ }
28
28
 
29
- get initial() {
30
- return initialState
31
- },
29
+ fresh.pages[location.pathname] = { ...fresh.pages[location.pathname], ...page.buildState() }
30
+ storage.save(key, fresh)
31
+ }
32
32
 
33
- get current() {
34
- return currentState
35
- },
33
+ const initialize = json => {
34
+ const state = { ...stub, ...JSON.parse(json) }
35
+ signed = state.signed
36
+ unsigned = observable(state.unsigned)
37
+ save()
38
+ dispatch(stateEvents.stateInitialize, document, { detail: unsigned })
39
+ }
36
40
 
37
- get changed() {
38
- return changedState
39
- },
41
+ // setup
42
+ addEventListener('DOMContentLoaded', restore)
43
+ addEventListener('turbo:morph', restore)
44
+ addEventListener('turbo:render', restore)
45
+ addEventListener('turbo:before-fetch-request', save)
46
+ addEventListener('beforeunload', save)
40
47
 
48
+ export default {
49
+ initialize,
50
+ buildPageState: page.buildState,
41
51
  get signed() {
42
- return signedState
52
+ return signed
53
+ },
54
+ get unsigned() {
55
+ return unsigned
43
56
  }
44
57
  }
@@ -12,7 +12,7 @@ function observable(object, parent = null) {
12
12
  return true
13
13
  },
14
14
 
15
- set(target, key, value, receiver) {
15
+ set(target, key, value, _receiver) {
16
16
  target[key] = observable(value, this)
17
17
  dispatch(events.stateChange, document, { detail: { state: head } })
18
18
  return true
@@ -0,0 +1,33 @@
1
+ import schema from '../schema.js'
2
+
3
+ const updateElement = (id, attribute, value, attempts = 1) => {
4
+ if (attempts > 20) return
5
+ const element = document.getElementById(id)
6
+ if (element?.isConnected) return element.setAttribute(attribute, value)
7
+ setTimeout(() => updateElement(id, attribute, value, attempts + 1), attempts * 5)
8
+ }
9
+
10
+ const buildState = () => {
11
+ const elements = Array.from(document.querySelectorAll(`[id][${schema.stateAttributesAttribute}]`))
12
+ return elements.reduce((memo, element) => {
13
+ const attributes = JSON.parse(element.getAttribute(schema.stateAttributesAttribute))
14
+ if (element.id) {
15
+ memo[element.id] = attributes.reduce((acc, name) => {
16
+ if (element.hasAttribute(name)) acc[name] = element.getAttribute(name) || name
17
+ return acc
18
+ }, {})
19
+ }
20
+ return memo
21
+ }, {})
22
+ }
23
+
24
+ const restoreState = (state = {}) => {
25
+ for (const [id, attributes] of Object.entries(state)) {
26
+ for (const [attribute, value] of Object.entries(attributes)) updateElement(id, attribute, value)
27
+ }
28
+ }
29
+
30
+ export default {
31
+ buildState,
32
+ restoreState
33
+ }
@@ -0,0 +1,11 @@
1
+ function save(name, value) {
2
+ if (typeof value !== 'object') value = {}
3
+ return localStorage.setItem(String(name), JSON.stringify(value))
4
+ }
5
+
6
+ function find(name) {
7
+ const stored = localStorage.getItem(String(name))
8
+ return stored ? JSON.parse(stored) : {}
9
+ }
10
+
11
+ export default { save, find }
@@ -1,6 +1,4 @@
1
1
  import headers from './headers'
2
- import lifecycle from './lifecycle'
3
- import { dispatch } from './events'
4
2
  import { render } from './renderer'
5
3
 
6
4
  const frameSources = {}
@@ -17,15 +15,7 @@ addEventListener('turbo:before-fetch-response', event => {
17
15
 
18
16
  // We'll take it from here Hotwire...
19
17
  event.preventDefault()
20
- const { statusCode } = response
21
18
  const { strategy } = headers.tokenize(header)
22
-
23
- // FAIL: Status outside the range of 200-399
24
- if (statusCode < 200 || statusCode > 399) {
25
- const error = `Server returned a ${status} status code! TurboBoost Commands require 2XX-3XX status codes.`
26
- dispatch(lifecycle.events.clientError, document, { detail: { error, response } }, true)
27
- }
28
-
29
19
  response.responseHTML.then(content => render(strategy, content))
30
20
  })
31
21
 
@@ -1 +1 @@
1
- export default '0.2.2'
1
+ export default '0.3.0'
@@ -36,6 +36,14 @@ class TurboBoost::Commands::AttributeSet
36
36
  end
37
37
  end
38
38
 
39
+ def include?(key)
40
+ instance_variable_defined?(:"@#{key}")
41
+ end
42
+
43
+ alias_method :has_key?, :include?
44
+ alias_method :key?, :include?
45
+ alias_method :member?, :include?
46
+
39
47
  def to_h
40
48
  instance_variables.each_with_object({}.with_indifferent_access) do |name, memo|
41
49
  value = instance_variable_get(name)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "attribute_set"
4
4
  require_relative "command_callbacks"
5
+ require_relative "command_validator"
5
6
 
6
7
  # TurboBoost::Commands::Command superclass.
7
8
  # All command classes should inherit from this class.
@@ -88,13 +89,17 @@ class TurboBoost::Commands::Command
88
89
  @state = state
89
90
  @params = params
90
91
  @turbo_streams = Set.new
92
+ resolve_state if TurboBoost::Commands.config.resolve_state
91
93
  end
92
94
 
93
- # Abstract method to resolve state (default noop), override in subclassed commands
94
- def resolve_state(client_state)
95
+ # Abstract method to resolve state (default: noop)
96
+ # Override in subclassed commands to resolve unsigned/optimistic client state with signed/server state
97
+ def resolve_state
95
98
  end
96
99
 
97
- # Abstract `perform` method, override in subclassed commands
100
+ # Abstract `perform` method
101
+ # Override in subclassed commands
102
+ # @raise [NotImplementedError]
98
103
  def perform
99
104
  raise NotImplementedError, "#{self.class.name} must implement the `perform` method!"
100
105
  end
@@ -86,15 +86,28 @@ module TurboBoost::Commands::CommandCallbacks
86
86
  end
87
87
 
88
88
  def perform_with_callbacks(method_name)
89
+ # Setup a temporary `rescue_from` handler on the controller to trap command errors because commands are
90
+ # run in a controller `before_action` callback. This allows us to properly handle command errors here
91
+ # instead of letting Rails return the normal 500 error page.
92
+ command = self
93
+ controller.class.rescue_from Exception, with: ->(error) { command.send :internal_error_handler, error }
94
+
89
95
  @performing_method_name = method_name
90
- run_callbacks NAME do
91
- public_send method_name
92
- performed!
96
+ begin
97
+ run_callbacks NAME do
98
+ public_send method_name
99
+ performed!
100
+ end
101
+ ensure
102
+ @performing_method_name = nil
103
+ end
104
+
105
+ # Tear down the temporary `rescue_from` handler
106
+ controller.rescue_handlers.reject! do |handler|
107
+ handler.to_s.include? "turbo_boost/commands/command_callbacks.rb"
93
108
  end
94
109
  rescue => error
95
- errored! TurboBoost::Commands::PerformError.new(command: self, cause: error)
96
- ensure
97
- @performing_method_name = nil
110
+ internal_error_handler error
98
111
  end
99
112
 
100
113
  def aborted?
@@ -147,4 +160,8 @@ module TurboBoost::Commands::CommandCallbacks
147
160
  changed @performed = true
148
161
  notify_observers :performed
149
162
  end
163
+
164
+ def internal_error_handler(error)
165
+ errored! TurboBoost::Commands::PerformError.new(command: self, cause: error)
166
+ end
150
167
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TurboBoost::Commands::CommandValidator
4
+ def initialize(command, method_name)
5
+ @command = command
6
+ @method_name = method_name.to_sym
7
+ end
8
+
9
+ attr_reader :command, :method_name
10
+
11
+ def validate
12
+ valid_class? && valid_method?
13
+ end
14
+ alias_method :valid?, :validate
15
+
16
+ def validate!
17
+ message = "`#{command.class.name}` is not a subclass of `#{TurboBoost::Commands::Command.name}`!"
18
+ raise TurboBoost::Commands::InvalidClassError.new(message, command: command.class) unless valid_class?
19
+
20
+ message = "`#{command.class.name}` does not define the public method `#{method_name}`!"
21
+ raise TurboBoost::Commands::InvalidMethodError.new(message, command: command.class) unless valid_method?
22
+
23
+ true
24
+ end
25
+
26
+ private
27
+
28
+ def command_ancestors
29
+ range = 0..(command.class.ancestors.index(TurboBoost::Commands::Command).to_i - 1)
30
+ command.class.ancestors.[](range) || []
31
+ end
32
+
33
+ def valid_class?
34
+ command.class.ancestors.include? TurboBoost::Commands::Command
35
+ end
36
+
37
+ def valid_method?
38
+ return false unless valid_class?
39
+ return false unless command_ancestors.any? { |a| a.public_instance_methods(false).any? method_name }
40
+
41
+ method = command.class.public_instance_method(method_name)
42
+ method&.parameters&.none?
43
+ end
44
+ end
@@ -5,27 +5,27 @@ require_relative "runner"
5
5
  class TurboBoost::Commands::ControllerPack
6
6
  include TurboBoost::Commands::AttributeHydration
7
7
 
8
+ def initialize(controller)
9
+ @runner = TurboBoost::Commands::Runner.new(controller)
10
+ @command = runner.command_instance
11
+ end
12
+
8
13
  attr_reader :runner, :command
9
14
 
10
15
  delegate(
11
- :command_state,
12
16
  :command_aborted?,
13
17
  :command_errored?,
14
18
  :command_performed?,
15
19
  :command_performing?,
16
20
  :command_requested?,
17
21
  :command_succeeded?,
18
- :controller,
22
+ :state,
19
23
  to: :runner
20
24
  )
21
25
 
22
- def initialize(controller)
23
- @runner = TurboBoost::Commands::Runner.new(controller)
24
- @command = runner.command_instance
25
- end
26
-
27
- def state
28
- ActiveSupport::Deprecation.warn "The `state` method has been deprecated. Please update to `command_state`."
29
- command_state
26
+ # DEPRECATED: This method will removed in a future release
27
+ def controller
28
+ ActiveSupport::Deprecation.warn "This method will removed in a future release."
29
+ runner.controller
30
30
  end
31
31
  end
@@ -7,7 +7,9 @@ require_relative "version"
7
7
  require_relative "http_status_codes"
8
8
  require_relative "errors"
9
9
  require_relative "patches"
10
- require_relative "middleware"
10
+ require_relative "sanitizer"
11
+ require_relative "middlewares/entry_middleware"
12
+ require_relative "middlewares/exit_middleware"
11
13
  require_relative "command"
12
14
  require_relative "controller_pack"
13
15
  require_relative "../../../app/controllers/concerns/turbo_boost/commands/controller"
@@ -20,16 +22,18 @@ module TurboBoost::Commands
20
22
 
21
23
  class Engine < ::Rails::Engine
22
24
  config.turbo_boost_commands = ActiveSupport::OrderedOptions.new
23
- config.turbo_boost_commands[:protect_from_forgery] = true
24
- config.turbo_boost_commands[:precompile_assets] = true
25
-
26
- # must opt-in to state overrides
27
- config.turbo_boost_commands[:apply_client_state_overrides] = false
28
- config.turbo_boost_commands[:apply_server_state_overrides] = false
29
-
30
- initializer "turbo_boost_commands.configuration" do |app|
25
+ config.turbo_boost_commands[:alert_on_abort] = false # (true, false, "development", "test", "production")
26
+ config.turbo_boost_commands[:alert_on_error] = false # (true, false, "development", "test", "production")
27
+ config.turbo_boost_commands[:precompile_assets] = true # (true, false)
28
+ config.turbo_boost_commands[:protect_from_forgery] = false # (true, false) TODO: Support override in Commands
29
+ config.turbo_boost_commands[:raise_on_invalid_command] = "development" # (true, false, "development", "test", "production")
30
+ config.turbo_boost_commands[:resolve_state] = false # (true, false)
31
+
32
+ initializer "turbo_boost_commands.configuration", before: :build_middleware_stack do |app|
31
33
  Mime::Type.register "text/vnd.turbo-boost.html", :turbo_boost
32
- app.middleware.insert 0, TurboBoost::Commands::Middleware
34
+
35
+ app.middleware.insert 0, TurboBoost::Commands::EntryMiddleware
36
+ app.middleware.use TurboBoost::Commands::ExitMiddleware
33
37
 
34
38
  ActiveSupport.on_load :action_controller_base do
35
39
  # `self` is ActionController::Base
@@ -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!"