motion 0.1.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.
- checksums.yaml +7 -0
- data/lib/generators/motion/install_generator.rb +27 -0
- data/lib/generators/motion/templates/motion.rb +22 -0
- data/lib/generators/motion/templates/motion_controller.js +28 -0
- data/lib/motion/action_cable_extentions/declarative_streams.rb +104 -0
- data/lib/motion/action_cable_extentions/log_suppression.rb +33 -0
- data/lib/motion/action_cable_extentions.rb +13 -0
- data/lib/motion/channel.rb +88 -0
- data/lib/motion/component/broadcasts.rb +88 -0
- data/lib/motion/component/lifecycle.rb +31 -0
- data/lib/motion/component/motions.rb +60 -0
- data/lib/motion/component/rendering.rb +66 -0
- data/lib/motion/component.rb +21 -0
- data/lib/motion/component_connection.rb +98 -0
- data/lib/motion/configuration.rb +106 -0
- data/lib/motion/element.rb +70 -0
- data/lib/motion/errors.rb +148 -0
- data/lib/motion/event.rb +41 -0
- data/lib/motion/log_helper.rb +74 -0
- data/lib/motion/markup_transformer.rb +65 -0
- data/lib/motion/railtie.rb +11 -0
- data/lib/motion/serializer.rb +100 -0
- data/lib/motion/test_helpers.rb +29 -0
- data/lib/motion/version.rb +5 -0
- data/lib/motion.rb +54 -0
- metadata +103 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
class Configuration
|
7
|
+
class << self
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
def default
|
11
|
+
@default ||= new
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_writer :options
|
17
|
+
|
18
|
+
def option(option, &default)
|
19
|
+
define_option_reader(option, &default)
|
20
|
+
define_option_writer(option)
|
21
|
+
|
22
|
+
self.options = [*options, option].freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def define_option_reader(option, &default)
|
26
|
+
define_method(option) do
|
27
|
+
if instance_variable_defined?(:"@#{option}")
|
28
|
+
instance_variable_get(:"@#{option}")
|
29
|
+
else
|
30
|
+
instance_variable_set(:"@#{option}", instance_exec(&default))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_option_writer(option)
|
36
|
+
define_method(:"#{option}=") do |value|
|
37
|
+
raise AlreadyConfiguredError if @finalized
|
38
|
+
|
39
|
+
instance_variable_set(:"@#{option}", value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
yield self if block_given?
|
46
|
+
|
47
|
+
# Ensure a value is selected for all options
|
48
|
+
self.class.options.each(&method(:public_send))
|
49
|
+
|
50
|
+
# Prevent further changes
|
51
|
+
@finalized = true
|
52
|
+
end
|
53
|
+
|
54
|
+
# //////////////////////////////////////////////////////////////////////////
|
55
|
+
|
56
|
+
option :secret do
|
57
|
+
require "rails"
|
58
|
+
|
59
|
+
Rails.application.key_generator.generate_key("motion:secret")
|
60
|
+
end
|
61
|
+
|
62
|
+
option :revision do
|
63
|
+
warn <<~MSG # TODO: Better message (Focus on "How do I fix this?")
|
64
|
+
Motion is automatically inferring the application's revision from git.
|
65
|
+
Depending on your deployment, this may not work for you in production.
|
66
|
+
If it does, add "config.revision = `git rev-parse HEAD`.chomp" to your
|
67
|
+
Motion initializer. If it does not, do something else (probably read an
|
68
|
+
env var or something).
|
69
|
+
MSG
|
70
|
+
|
71
|
+
`git rev-parse HEAD`.chomp
|
72
|
+
end
|
73
|
+
|
74
|
+
option :renderer_for_connection_proc do
|
75
|
+
->(websocket_connection) do
|
76
|
+
require "rack"
|
77
|
+
require "action_controller"
|
78
|
+
|
79
|
+
# Make a special effort to use the host application's base controller
|
80
|
+
# in case the CSRF protection has been customized, but don't couple to
|
81
|
+
# a particular constant from the outer application.
|
82
|
+
controller =
|
83
|
+
if defined?(ApplicationController)
|
84
|
+
ApplicationController
|
85
|
+
else
|
86
|
+
ActionController::Base
|
87
|
+
end
|
88
|
+
|
89
|
+
controller.renderer.new(
|
90
|
+
websocket_connection.env.slice(
|
91
|
+
Rack::HTTP_COOKIE,
|
92
|
+
Rack::RACK_SESSION
|
93
|
+
)
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
option(:stimulus_controller_identifier) { "motion" }
|
99
|
+
option(:key_attribute) { "data-motion-key" }
|
100
|
+
option(:state_attribute) { "data-motion-state" }
|
101
|
+
|
102
|
+
# This is included for completeness. It is not currently used internally by
|
103
|
+
# Motion, but it might be required for building view helpers in the future.
|
104
|
+
option(:motion_attribute) { "data-motion" }
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
class Element
|
7
|
+
def self.from_raw(raw)
|
8
|
+
new(raw) if raw
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :raw
|
12
|
+
|
13
|
+
def initialize(raw)
|
14
|
+
@raw = raw.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def tag_name
|
18
|
+
raw["tagName"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def value
|
22
|
+
raw["value"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def attributes
|
26
|
+
raw.fetch("attributes", {})
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
key = key.to_s
|
31
|
+
|
32
|
+
attributes[key] || attributes[key.tr("_", "-")]
|
33
|
+
end
|
34
|
+
|
35
|
+
def id
|
36
|
+
self[:id]
|
37
|
+
end
|
38
|
+
|
39
|
+
class DataAttributes
|
40
|
+
attr_reader :element
|
41
|
+
|
42
|
+
def initialize(element)
|
43
|
+
@element = element
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](data)
|
47
|
+
element["data-#{data}"]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private_constant :DataAttributes
|
52
|
+
|
53
|
+
def data
|
54
|
+
return @data if defined?(@data)
|
55
|
+
|
56
|
+
@data = DataAttributes.new(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def form_data
|
60
|
+
return @form_data if defined?(@form_data)
|
61
|
+
|
62
|
+
@form_data =
|
63
|
+
ActionController::Parameters.new(
|
64
|
+
Rack::Utils.parse_nested_query(
|
65
|
+
raw.fetch("formData", "")
|
66
|
+
)
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
class ComponentError < Error
|
9
|
+
attr_reader :component
|
10
|
+
|
11
|
+
def initialize(component, message = nil)
|
12
|
+
super(message)
|
13
|
+
@component = component
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class ComponentRenderingError < ComponentError; end
|
18
|
+
|
19
|
+
class MotionNotMapped < ComponentError
|
20
|
+
attr_reader :motion
|
21
|
+
|
22
|
+
def initialize(component, motion)
|
23
|
+
super(component, <<~MSG)
|
24
|
+
No component motion handler mapped for motion '#{motion}' in component #{component.class}.
|
25
|
+
|
26
|
+
Fix: Add the following to #{component.class}:
|
27
|
+
|
28
|
+
map_motion :#{motion}
|
29
|
+
MSG
|
30
|
+
|
31
|
+
@motion = motion
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class BlockNotAllowedError < ComponentRenderingError
|
36
|
+
def initialize(component)
|
37
|
+
super(component, <<~MSG)
|
38
|
+
Motion does not support rendering with a block.
|
39
|
+
|
40
|
+
Fix: Use a plain component and wrap with a motion component.
|
41
|
+
MSG
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class MultipleRootsError < ComponentRenderingError
|
46
|
+
def initialize(component)
|
47
|
+
super(component, <<~MSG)
|
48
|
+
The template for #{component.class} can only have one root element.
|
49
|
+
|
50
|
+
Fix: Wrap all elements in a single element, such as <div> or <section>.
|
51
|
+
MSG
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class InvalidComponentStateError < ComponentError; end
|
56
|
+
|
57
|
+
class UnrepresentableStateError < InvalidComponentStateError
|
58
|
+
def initialize(component, cause)
|
59
|
+
super(component, <<~MSG)
|
60
|
+
Some state prevented #{component.class} from being serialized into a
|
61
|
+
string. Motion components must be serializable using Marshal.dump. Many
|
62
|
+
types of objects are not serializable including procs, references to
|
63
|
+
anonymous classes, and more. See the documentation for Marshal.dump for
|
64
|
+
more information.
|
65
|
+
|
66
|
+
Fix: Ensure that any exotic state variables in #{component.class} are
|
67
|
+
removed or replaced.
|
68
|
+
|
69
|
+
The specific (but probably useless) error from Marshal was: #{cause}
|
70
|
+
MSG
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class SerializedComponentError < Error; end
|
75
|
+
|
76
|
+
class InvalidSerializedStateError < SerializedComponentError
|
77
|
+
def initialize
|
78
|
+
super(<<~MSG)
|
79
|
+
The serialized state of your component is not valid.
|
80
|
+
|
81
|
+
Fix: Ensure that you have not tampered with the DOM.
|
82
|
+
MSG
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class IncorrectRevisionError < SerializedComponentError
|
87
|
+
attr_reader :expected_revision,
|
88
|
+
:actual_revision
|
89
|
+
|
90
|
+
def initialize(expected_revision, actual_revision)
|
91
|
+
super(<<~MSG)
|
92
|
+
Cannot mount a component from another version of the application.
|
93
|
+
|
94
|
+
Expected revision `#{expected_revision}`;
|
95
|
+
Got `#{actual_revision}`
|
96
|
+
|
97
|
+
Read more: https://github.com/unabridged/motion/wiki/IncorrectRevisionError
|
98
|
+
|
99
|
+
Fix:
|
100
|
+
* Avoid tampering with Motion DOM elements and data attributes (e.g. data-motion-state).
|
101
|
+
* In production, enforce a page refresh for pages with Motion components on deploy.
|
102
|
+
MSG
|
103
|
+
|
104
|
+
@expected_revision = expected_revision
|
105
|
+
@actual_revision = actual_revision
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class AlreadyConfiguredError < Error
|
110
|
+
def initialize
|
111
|
+
super(<<~MSG)
|
112
|
+
Motion is already configured.
|
113
|
+
|
114
|
+
Fix: Move all Motion config to config/initializers/motion.rb.
|
115
|
+
MSG
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class IncompatibleClientError < Error
|
120
|
+
attr_reader :expected_version,
|
121
|
+
:actual_version
|
122
|
+
|
123
|
+
def initialize(expected_version, actual_version)
|
124
|
+
super(<<~MSG)
|
125
|
+
Expected client version #{expected_version}, but got #{actual_version}.
|
126
|
+
|
127
|
+
Fix: Run `bin/yarn add @unabridged/motion@#{expected_version}`
|
128
|
+
MSG
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class BadSecretError < Error
|
133
|
+
attr_reader :minimum_bytes
|
134
|
+
|
135
|
+
def initialize(minimum_bytes)
|
136
|
+
super(<<~MSG)
|
137
|
+
The secret that you provided is not long enough. It must have at least
|
138
|
+
#{minimum_bytes} bytes.
|
139
|
+
MSG
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class BadRevisionError < Error
|
144
|
+
def initialize
|
145
|
+
super("The revision cannot contain a NULL byte")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/motion/event.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
class Event
|
7
|
+
def self.from_raw(raw)
|
8
|
+
new(raw) if raw
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :raw
|
12
|
+
|
13
|
+
def initialize(raw)
|
14
|
+
@raw = raw.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
raw["type"]
|
19
|
+
end
|
20
|
+
|
21
|
+
alias name type
|
22
|
+
|
23
|
+
def details
|
24
|
+
raw.fetch("details", {})
|
25
|
+
end
|
26
|
+
|
27
|
+
def extra_data
|
28
|
+
raw["extraData"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def target
|
32
|
+
return @target if defined?(@target)
|
33
|
+
|
34
|
+
@target = Motion::Element.from_raw(raw["target"])
|
35
|
+
end
|
36
|
+
|
37
|
+
def form_data
|
38
|
+
target&.form_data
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "active_support/core_ext/string/indent"
|
5
|
+
|
6
|
+
require "motion"
|
7
|
+
|
8
|
+
module Motion
|
9
|
+
class LogHelper
|
10
|
+
BACKTRACE_FRAMES = 5
|
11
|
+
DEFAULT_TAG = "Motion"
|
12
|
+
|
13
|
+
def self.for_channel(channel, logger: channel.connection.logger)
|
14
|
+
new(logger: logger, tag: DEFAULT_TAG)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.for_component(component, logger: nil)
|
18
|
+
new(logger: logger, tag: "#{component.class}:#{component.object_id}")
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :logger, :tag
|
22
|
+
|
23
|
+
def initialize(logger: nil, tag: nil)
|
24
|
+
@logger = logger || Rails.logger
|
25
|
+
@tag = tag || DEFAULT_TAG
|
26
|
+
end
|
27
|
+
|
28
|
+
def error(message, error: nil)
|
29
|
+
error_info = error ? ":\n#{indent(format_exception(error))}" : ""
|
30
|
+
|
31
|
+
logger.error("[#{tag}] #{message}#{error_info}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def info(message)
|
35
|
+
logger.info("[#{tag}] #{message}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def timing(message)
|
39
|
+
start_time = Time.now
|
40
|
+
result = yield
|
41
|
+
end_time = Time.now
|
42
|
+
|
43
|
+
info("#{message} (in #{format_duration(end_time - start_time)})")
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def for_component(component)
|
49
|
+
self.class.for_component(component, logger: logger)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def format_exception(exception)
|
55
|
+
frames = exception.backtrace.first(BACKTRACE_FRAMES).join("\n")
|
56
|
+
|
57
|
+
"#{exception.class}: #{exception}\n#{indent(frames)}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def format_duration(duration)
|
61
|
+
duration_ms = duration * 1000
|
62
|
+
|
63
|
+
if duration_ms < 0.1
|
64
|
+
"less than 0.1ms"
|
65
|
+
else
|
66
|
+
"#{duration_ms.round(1)}ms"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def indent(string)
|
71
|
+
string.indent(1, "\t")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
require "motion"
|
6
|
+
|
7
|
+
module Motion
|
8
|
+
class MarkupTransformer
|
9
|
+
STIMULUS_CONTROLLER_ATTRIBUTE = "data-controller"
|
10
|
+
|
11
|
+
attr_reader :serializer,
|
12
|
+
:stimulus_controller_identifier,
|
13
|
+
:key_attribute,
|
14
|
+
:state_attribute
|
15
|
+
|
16
|
+
def initialize(
|
17
|
+
serializer: Motion.serializer,
|
18
|
+
stimulus_controller_identifier:
|
19
|
+
Motion.config.stimulus_controller_identifier,
|
20
|
+
key_attribute: Motion.config.key_attribute,
|
21
|
+
state_attribute: Motion.config.state_attribute
|
22
|
+
)
|
23
|
+
@serializer = serializer
|
24
|
+
@stimulus_controller_identifier = stimulus_controller_identifier
|
25
|
+
@key_attribute = key_attribute
|
26
|
+
@state_attribute = state_attribute
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_state_to_html(component, html)
|
30
|
+
key, state = serializer.serialize(component)
|
31
|
+
|
32
|
+
transform_root(component, html) do |root|
|
33
|
+
root[STIMULUS_CONTROLLER_ATTRIBUTE] =
|
34
|
+
values(
|
35
|
+
stimulus_controller_identifier,
|
36
|
+
root[STIMULUS_CONTROLLER_ATTRIBUTE]
|
37
|
+
)
|
38
|
+
|
39
|
+
root[key_attribute] = key
|
40
|
+
root[state_attribute] = state
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def transform_root(component, html)
|
47
|
+
fragment = Nokogiri::HTML::DocumentFragment.parse(html)
|
48
|
+
root, *unexpected_others = fragment.children
|
49
|
+
|
50
|
+
raise MultipleRootsError, component if unexpected_others.any?(&:present?)
|
51
|
+
|
52
|
+
yield root
|
53
|
+
|
54
|
+
fragment.to_html.html_safe
|
55
|
+
end
|
56
|
+
|
57
|
+
def values(*values, delimiter: " ")
|
58
|
+
values
|
59
|
+
.compact
|
60
|
+
.flat_map { |value| value.split(delimiter) }
|
61
|
+
.uniq
|
62
|
+
.join(delimiter)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
require "active_support/message_encryptor"
|
5
|
+
|
6
|
+
require "motion"
|
7
|
+
|
8
|
+
module Motion
|
9
|
+
class Serializer
|
10
|
+
HASH_PEPPER = "Motion"
|
11
|
+
private_constant :HASH_PEPPER
|
12
|
+
|
13
|
+
NULL_BYTE = "\0"
|
14
|
+
|
15
|
+
attr_reader :secret, :revision
|
16
|
+
|
17
|
+
def self.minimum_secret_byte_length
|
18
|
+
ActiveSupport::MessageEncryptor.key_len
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(
|
22
|
+
secret: Motion.config.secret,
|
23
|
+
revision: Motion.config.revision
|
24
|
+
)
|
25
|
+
unless secret.each_byte.count >= self.class.minimum_secret_byte_length
|
26
|
+
raise BadSecretError.new(self.class.minimum_secret_byte_length)
|
27
|
+
end
|
28
|
+
|
29
|
+
raise BadRevisionError if revision.include?(NULL_BYTE)
|
30
|
+
|
31
|
+
@secret = secret
|
32
|
+
@revision = revision
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialize(component)
|
36
|
+
state = dump(component)
|
37
|
+
state_with_revision = "#{revision}#{NULL_BYTE}#{state}"
|
38
|
+
|
39
|
+
[
|
40
|
+
salted_digest(state_with_revision),
|
41
|
+
encrypt_and_sign(state_with_revision)
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
def deserialize(serialized_component)
|
46
|
+
state_with_revision = decrypt_and_verify(serialized_component)
|
47
|
+
serialized_revision, state = state_with_revision.split(NULL_BYTE, 2)
|
48
|
+
component = load(state)
|
49
|
+
|
50
|
+
if revision == serialized_revision
|
51
|
+
component
|
52
|
+
else
|
53
|
+
component.class.upgrade_from(serialized_revision, component)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def dump(component)
|
60
|
+
Marshal.dump(component)
|
61
|
+
rescue TypeError => e
|
62
|
+
raise UnrepresentableStateError.new(component, e.message)
|
63
|
+
end
|
64
|
+
|
65
|
+
def load(state)
|
66
|
+
Marshal.load(state)
|
67
|
+
end
|
68
|
+
|
69
|
+
def encrypt_and_sign(cleartext)
|
70
|
+
encryptor.encrypt_and_sign(cleartext)
|
71
|
+
end
|
72
|
+
|
73
|
+
def decrypt_and_verify(cypertext)
|
74
|
+
encryptor.decrypt_and_verify(cypertext)
|
75
|
+
rescue ActiveSupport::MessageEncryptor::InvalidMessage,
|
76
|
+
ActiveSupport::MessageVerifier::InvalidSignature
|
77
|
+
raise InvalidSerializedStateError
|
78
|
+
end
|
79
|
+
|
80
|
+
def salted_digest(input)
|
81
|
+
Digest::SHA256.base64digest(hash_salt + input)
|
82
|
+
end
|
83
|
+
|
84
|
+
def encryptor
|
85
|
+
@encryptor ||= ActiveSupport::MessageEncryptor.new(derive_encryptor_key)
|
86
|
+
end
|
87
|
+
|
88
|
+
def hash_salt
|
89
|
+
@hash_salt ||= derive_hash_salt
|
90
|
+
end
|
91
|
+
|
92
|
+
def derive_encryptor_key
|
93
|
+
secret.byteslice(0, self.class.minimum_secret_byte_length)
|
94
|
+
end
|
95
|
+
|
96
|
+
def derive_hash_salt
|
97
|
+
Digest::SHA256.digest(HASH_PEPPER + secret)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
module TestHelpers
|
7
|
+
def assert_motion(component, motion_name)
|
8
|
+
assert motion?(component, motion_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def refute_motion(component, motion_name)
|
12
|
+
refute motion?(component, motion_name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def motion?(component, motion_name)
|
16
|
+
component.motions.include?(motion_name.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run_motion(component, motion_name)
|
20
|
+
if block_given?
|
21
|
+
c = component.dup
|
22
|
+
c.process_motion(motion_name.to_s)
|
23
|
+
yield c
|
24
|
+
else
|
25
|
+
component.process_motion(motion_name.to_s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/motion.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion/version"
|
4
|
+
require "motion/errors"
|
5
|
+
|
6
|
+
module Motion
|
7
|
+
autoload :ActionCableExtentions, "motion/action_cable_extentions"
|
8
|
+
autoload :Channel, "motion/channel"
|
9
|
+
autoload :Component, "motion/component"
|
10
|
+
autoload :ComponentConnection, "motion/component_connection"
|
11
|
+
autoload :Configuration, "motion/configuration"
|
12
|
+
autoload :Element, "motion/element"
|
13
|
+
autoload :Event, "motion/event"
|
14
|
+
autoload :LogHelper, "motion/log_helper"
|
15
|
+
autoload :MarkupTransformer, "motion/markup_transformer"
|
16
|
+
autoload :Railtie, "motion/railtie"
|
17
|
+
autoload :Serializer, "motion/serializer"
|
18
|
+
autoload :TestHelpers, "motion/test_helpers"
|
19
|
+
|
20
|
+
def self.configure(&block)
|
21
|
+
raise AlreadyConfiguredError if @config
|
22
|
+
|
23
|
+
@config = Configuration.new(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.config
|
27
|
+
@config ||= Configuration.default
|
28
|
+
end
|
29
|
+
|
30
|
+
singleton_class.alias_method :configuration, :config
|
31
|
+
|
32
|
+
def self.serializer
|
33
|
+
@serializer ||= Serializer.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.markup_transformer
|
37
|
+
@markup_transformer ||= MarkupTransformer.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.build_renderer_for(websocket_connection)
|
41
|
+
config.renderer_for_connection_proc.call(websocket_connection)
|
42
|
+
end
|
43
|
+
|
44
|
+
# This method only exists for testing. Changing configuration while Motion is
|
45
|
+
# in use is not supported. It is only safe to call this method when no
|
46
|
+
# components are currently mounted.
|
47
|
+
def self.reset_internal_state_for_testing!(new_configuration = nil)
|
48
|
+
@config = new_configuration
|
49
|
+
@serializer = nil
|
50
|
+
@markup_transformer = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
require "motion/railtie" if defined?(Rails)
|