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.
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!"