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
data/app/javascript/index.js
CHANGED
@@ -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
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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 =
|
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 =
|
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
|
data/app/javascript/invoker.js
CHANGED
@@ -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
|
10
|
-
dispatch(lifecycle.events.clientError, document, { detail: { error
|
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
|
|
data/app/javascript/lifecycle.js
CHANGED
@@ -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.
|
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
|
-
|
11
|
-
addEventListener(
|
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 }
|
data/app/javascript/logger.js
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
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
|
-
|
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
|
|
data/app/javascript/renderer.js
CHANGED
@@ -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
|
-
|
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
|
15
|
-
|
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 }
|
data/app/javascript/schema.js
CHANGED
@@ -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:
|
1
|
+
// TODO: Move State to its own library
|
2
2
|
import observable from './observable'
|
3
|
-
import
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
29
|
+
fresh.pages[location.pathname] = { ...fresh.pages[location.pathname], ...page.buildState() }
|
30
|
+
storage.save(key, fresh)
|
31
|
+
}
|
32
32
|
|
33
|
-
|
34
|
-
|
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
|
-
|
38
|
-
|
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
|
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,
|
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 }
|
data/app/javascript/turbo.js
CHANGED
@@ -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
|
|
data/app/javascript/version.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export default '0.
|
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)
|
94
|
-
|
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
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
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
|
-
:
|
22
|
+
:state,
|
19
23
|
to: :runner
|
20
24
|
)
|
21
25
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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 "
|
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[:
|
24
|
-
config.turbo_boost_commands[:
|
25
|
-
|
26
|
-
#
|
27
|
-
config.turbo_boost_commands[:
|
28
|
-
config.turbo_boost_commands[:
|
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
|
-
|
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
|
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
|
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!"
|