solid_objects 0.4.3 → 0.5.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/CHANGELOG.md +9 -0
- data/README.md +54 -7
- data/app/assets/javascripts/solid_objects/component_refresh.js +124 -0
- data/app/controllers/solid_objects/components_controller.rb +2 -7
- data/app/helpers/solid_objects/actor_helper.rb +8 -1
- data/docs/adr/0009-realtime-updates.md +18 -1
- data/docs/architecture.md +34 -18
- data/docs/authorization.md +24 -0
- data/docs/correctness.md +12 -4
- data/docs/realtime.md +78 -13
- data/docs/roadmap.md +6 -5
- data/examples/application/app/views/chat_rooms/show.html.erb +3 -1
- data/lib/solid_objects/actor_view.rb +40 -13
- data/lib/solid_objects/component_registration.rb +61 -16
- data/lib/solid_objects/component_renderer.rb +14 -17
- data/lib/solid_objects/component_subscriptions.rb +7 -7
- data/lib/solid_objects/component_token.rb +70 -2
- data/lib/solid_objects/dom_identity.rb +12 -3
- data/lib/solid_objects/engine.rb +7 -0
- data/lib/solid_objects/turbo_stream_renderer.rb +14 -5
- data/lib/solid_objects/version.rb +1 -1
- data/sig/generated/lib/solid_objects/actor_view.rbs +10 -4
- data/sig/generated/lib/solid_objects/component_registration.rbs +34 -10
- data/sig/generated/lib/solid_objects/component_renderer.rbs +4 -8
- data/sig/generated/lib/solid_objects/component_token.rbs +24 -2
- data/sig/generated/lib/solid_objects/dom_identity.rbs +5 -2
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +3 -0
- metadata +2 -1
|
@@ -37,10 +37,20 @@ module SolidObjects
|
|
|
37
37
|
)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?) -> untyped
|
|
41
|
-
def component(
|
|
40
|
+
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
|
|
41
|
+
def component(
|
|
42
|
+
name,
|
|
43
|
+
observes: nil,
|
|
44
|
+
partial: nil,
|
|
45
|
+
key: nil,
|
|
46
|
+
locals: {},
|
|
47
|
+
refresh: :replace
|
|
48
|
+
)
|
|
42
49
|
component_name = normalized_component_name(name)
|
|
43
|
-
|
|
50
|
+
unless observes
|
|
51
|
+
validate_static_options!(key:, locals:, refresh:)
|
|
52
|
+
return static_component(component_name, partial:)
|
|
53
|
+
end
|
|
44
54
|
if partial
|
|
45
55
|
raise ArgumentError,
|
|
46
56
|
"reactive components resolve partials by actor and component name"
|
|
@@ -48,19 +58,21 @@ module SolidObjects
|
|
|
48
58
|
|
|
49
59
|
dependencies = normalized_dependencies(observes)
|
|
50
60
|
validate_dependencies!(dependencies)
|
|
51
|
-
ensure_unique_component!(component_name)
|
|
52
61
|
refresh_path = component_path_resolver.call(view_context:)
|
|
53
62
|
registration = ComponentRegistration.issue(
|
|
54
63
|
reference:,
|
|
55
64
|
component_name:,
|
|
65
|
+
component_key: key,
|
|
56
66
|
dependencies:,
|
|
67
|
+
locals:,
|
|
68
|
+
refresh_method: refresh,
|
|
57
69
|
snapshot:,
|
|
58
70
|
refresh_path:
|
|
59
71
|
)
|
|
72
|
+
ensure_unique_component!(registration)
|
|
60
73
|
rendered = ComponentRenderer.new(
|
|
61
74
|
snapshot:,
|
|
62
|
-
|
|
63
|
-
dependencies:,
|
|
75
|
+
registration:,
|
|
64
76
|
view_context:,
|
|
65
77
|
authorization_context:
|
|
66
78
|
).call
|
|
@@ -68,9 +80,10 @@ module SolidObjects
|
|
|
68
80
|
view_context.content_tag(
|
|
69
81
|
:"turbo-frame",
|
|
70
82
|
rendered,
|
|
71
|
-
id:
|
|
83
|
+
id: registration.dom_id,
|
|
72
84
|
data: {
|
|
73
|
-
solid_objects_revision: "#{snapshot.instance_id}:#{snapshot.revision}"
|
|
85
|
+
solid_objects_revision: "#{snapshot.instance_id}:#{snapshot.revision}",
|
|
86
|
+
solid_objects_refresh: registration.refresh_method
|
|
74
87
|
}
|
|
75
88
|
)
|
|
76
89
|
end
|
|
@@ -85,6 +98,11 @@ module SolidObjects
|
|
|
85
98
|
observable_names.dup
|
|
86
99
|
end
|
|
87
100
|
|
|
101
|
+
# @rbs () -> bool
|
|
102
|
+
def morph_components?
|
|
103
|
+
component_registrations.any?(&:morph?)
|
|
104
|
+
end
|
|
105
|
+
|
|
88
106
|
# @rbs () -> State
|
|
89
107
|
def state
|
|
90
108
|
snapshot.actor.state
|
|
@@ -183,14 +201,23 @@ module SolidObjects
|
|
|
183
201
|
"unknown observable dependency #{unknown.inspect} for #{reference.actor_type}"
|
|
184
202
|
end
|
|
185
203
|
|
|
186
|
-
# @rbs (
|
|
187
|
-
def ensure_unique_component!(
|
|
188
|
-
return unless component_registrations.any? do |
|
|
189
|
-
|
|
204
|
+
# @rbs (ComponentRegistration) -> void
|
|
205
|
+
def ensure_unique_component!(registration)
|
|
206
|
+
return unless component_registrations.any? do |existing_registration|
|
|
207
|
+
existing_registration.dom_id == registration.dom_id
|
|
190
208
|
end
|
|
191
209
|
|
|
192
210
|
raise ArgumentError,
|
|
193
|
-
"component #{component_name.inspect}
|
|
211
|
+
"component #{registration.component_name.inspect} with key " \
|
|
212
|
+
"#{registration.component_key.inspect} is already rendered in this solid_object scope"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
|
|
216
|
+
def validate_static_options!(key:, locals:, refresh:)
|
|
217
|
+
return if key.nil? && locals.empty? && refresh.to_s == "replace"
|
|
218
|
+
|
|
219
|
+
raise ArgumentError,
|
|
220
|
+
"key, locals, and refresh require an observable component"
|
|
194
221
|
end
|
|
195
222
|
|
|
196
223
|
# @rbs () -> Proc | ComponentPathResolver
|
|
@@ -6,7 +6,10 @@ module SolidObjects
|
|
|
6
6
|
class ComponentRegistration
|
|
7
7
|
# @rbs @reference: Reference
|
|
8
8
|
# @rbs @component_name: String
|
|
9
|
+
# @rbs @component_key: String | Integer?
|
|
9
10
|
# @rbs @dependencies: Array[String]
|
|
11
|
+
# @rbs @locals: Hash[String, untyped]
|
|
12
|
+
# @rbs @refresh_method: String
|
|
10
13
|
# @rbs @instance_id: Integer
|
|
11
14
|
# @rbs @revision: Integer
|
|
12
15
|
# @rbs @refresh_path: String
|
|
@@ -14,17 +17,23 @@ module SolidObjects
|
|
|
14
17
|
|
|
15
18
|
attr_reader :reference,
|
|
16
19
|
:component_name,
|
|
20
|
+
:component_key,
|
|
17
21
|
:dependencies,
|
|
22
|
+
:locals,
|
|
23
|
+
:refresh_method,
|
|
18
24
|
:instance_id,
|
|
19
25
|
:revision,
|
|
20
26
|
:refresh_path,
|
|
21
27
|
:token
|
|
22
28
|
|
|
23
|
-
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
|
|
29
|
+
# @rbs (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
|
|
24
30
|
def initialize(
|
|
25
31
|
reference:,
|
|
26
32
|
component_name:,
|
|
33
|
+
component_key:,
|
|
27
34
|
dependencies:,
|
|
35
|
+
locals:,
|
|
36
|
+
refresh_method:,
|
|
28
37
|
instance_id:,
|
|
29
38
|
revision:,
|
|
30
39
|
refresh_path:,
|
|
@@ -32,7 +41,10 @@ module SolidObjects
|
|
|
32
41
|
)
|
|
33
42
|
@reference = reference
|
|
34
43
|
@component_name = component_name
|
|
44
|
+
@component_key = component_key
|
|
35
45
|
@dependencies = dependencies.freeze
|
|
46
|
+
@locals = Serialization.readonly_copy(locals)
|
|
47
|
+
@refresh_method = refresh_method
|
|
36
48
|
@instance_id = instance_id
|
|
37
49
|
@revision = revision
|
|
38
50
|
@refresh_path = refresh_path
|
|
@@ -40,30 +52,43 @@ module SolidObjects
|
|
|
40
52
|
end
|
|
41
53
|
|
|
42
54
|
class << self
|
|
43
|
-
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
|
|
44
|
-
def issue(
|
|
55
|
+
# @rbs (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
|
|
56
|
+
def issue(
|
|
57
|
+
reference:,
|
|
58
|
+
component_name:,
|
|
59
|
+
component_key:,
|
|
60
|
+
dependencies:,
|
|
61
|
+
locals:,
|
|
62
|
+
refresh_method:,
|
|
63
|
+
snapshot:,
|
|
64
|
+
refresh_path:
|
|
65
|
+
)
|
|
45
66
|
token = ComponentToken.generate(
|
|
46
67
|
reference:,
|
|
47
68
|
component_name:,
|
|
69
|
+
component_key:,
|
|
48
70
|
dependencies:,
|
|
71
|
+
locals:,
|
|
72
|
+
refresh_method:,
|
|
49
73
|
instance_id: snapshot.instance_id,
|
|
50
74
|
revision: snapshot.revision,
|
|
51
75
|
refresh_path:
|
|
52
76
|
)
|
|
53
|
-
|
|
54
|
-
reference:,
|
|
55
|
-
component_name:,
|
|
56
|
-
dependencies:,
|
|
57
|
-
instance_id: snapshot.instance_id,
|
|
58
|
-
revision: snapshot.revision,
|
|
59
|
-
refresh_path:,
|
|
60
|
-
token:
|
|
61
|
-
)
|
|
77
|
+
build(ComponentToken.verify(token), token:)
|
|
62
78
|
end
|
|
63
79
|
|
|
64
80
|
# @rbs (String) -> ComponentRegistration
|
|
65
81
|
def from_token(token)
|
|
66
82
|
payload = ComponentToken.verify(token)
|
|
83
|
+
build(payload, token:)
|
|
84
|
+
rescue UnknownActorType, UnknownComponentDependency => error
|
|
85
|
+
raise InvalidComponentToken, error.message
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# @rbs (Hash[String, untyped], token: String) -> ComponentRegistration
|
|
91
|
+
def build(payload, token:)
|
|
67
92
|
reference = Reference.new(
|
|
68
93
|
actor_type: payload.fetch("actor_type"),
|
|
69
94
|
actor_id: payload.fetch("actor_id")
|
|
@@ -73,18 +98,17 @@ module SolidObjects
|
|
|
73
98
|
new(
|
|
74
99
|
reference:,
|
|
75
100
|
component_name: payload.fetch("component_name"),
|
|
101
|
+
component_key: payload["component_key"],
|
|
76
102
|
dependencies:,
|
|
103
|
+
locals: payload.fetch("locals"),
|
|
104
|
+
refresh_method: payload.fetch("refresh_method"),
|
|
77
105
|
instance_id: payload.fetch("instance_id"),
|
|
78
106
|
revision: payload.fetch("revision"),
|
|
79
107
|
refresh_path: payload.fetch("refresh_path"),
|
|
80
108
|
token:
|
|
81
109
|
)
|
|
82
|
-
rescue UnknownActorType, UnknownComponentDependency => error
|
|
83
|
-
raise InvalidComponentToken, error.message
|
|
84
110
|
end
|
|
85
111
|
|
|
86
|
-
private
|
|
87
|
-
|
|
88
112
|
# @rbs (Reference, Array[String]) -> void
|
|
89
113
|
def validate_dependencies!(reference, dependencies)
|
|
90
114
|
actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
@@ -104,6 +128,27 @@ module SolidObjects
|
|
|
104
128
|
[ instance_id, revision ]
|
|
105
129
|
end
|
|
106
130
|
|
|
131
|
+
# @rbs () -> String
|
|
132
|
+
def dom_id
|
|
133
|
+
DomIdentity.component(
|
|
134
|
+
reference,
|
|
135
|
+
component_name,
|
|
136
|
+
key: component_key
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @rbs () -> bool
|
|
141
|
+
def morph?
|
|
142
|
+
refresh_method == "morph"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @rbs () -> Hash[String, untyped]
|
|
146
|
+
def authorization_arguments
|
|
147
|
+
return locals unless component_key
|
|
148
|
+
|
|
149
|
+
locals.merge("component_key" => component_key).freeze
|
|
150
|
+
end
|
|
151
|
+
|
|
107
152
|
# @rbs (Integer, Integer) -> String
|
|
108
153
|
def refresh_url(instance_id, revision)
|
|
109
154
|
query = URI.encode_www_form(
|
|
@@ -3,46 +3,44 @@
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class ComponentRenderer
|
|
5
5
|
# @rbs @snapshot: ActorSnapshot
|
|
6
|
-
# @rbs @
|
|
7
|
-
# @rbs @dependencies: Array[String]
|
|
6
|
+
# @rbs @registration: ComponentRegistration
|
|
8
7
|
# @rbs @view_context: untyped
|
|
9
8
|
# @rbs @authorization_context: untyped
|
|
10
9
|
|
|
11
|
-
# @rbs (snapshot: ActorSnapshot,
|
|
10
|
+
# @rbs (snapshot: ActorSnapshot, registration: ComponentRegistration, view_context: untyped, authorization_context: untyped) -> void
|
|
12
11
|
def initialize(
|
|
13
12
|
snapshot:,
|
|
14
|
-
|
|
15
|
-
dependencies:,
|
|
13
|
+
registration:,
|
|
16
14
|
view_context:,
|
|
17
15
|
authorization_context:
|
|
18
16
|
)
|
|
19
17
|
@snapshot = snapshot
|
|
20
|
-
@
|
|
21
|
-
@dependencies = dependencies
|
|
18
|
+
@registration = registration
|
|
22
19
|
@view_context = view_context
|
|
23
20
|
@authorization_context = authorization_context
|
|
24
21
|
end
|
|
25
22
|
|
|
26
23
|
# @rbs () -> untyped
|
|
27
24
|
def call
|
|
28
|
-
[ component_name, *dependencies ].uniq.each do |authorization_name|
|
|
25
|
+
[ registration.component_name, *registration.dependencies ].uniq.each do |authorization_name|
|
|
29
26
|
authorize_read!(authorization_name)
|
|
30
27
|
end
|
|
31
28
|
actor = ComponentView.new(
|
|
32
29
|
snapshot:,
|
|
33
|
-
dependencies
|
|
30
|
+
dependencies: registration.dependencies,
|
|
34
31
|
authorization_context:
|
|
35
32
|
)
|
|
36
33
|
view_context.render(
|
|
37
34
|
partial: default_partial,
|
|
38
|
-
locals:
|
|
35
|
+
locals: registration.locals.transform_keys(&:to_sym).merge(
|
|
39
36
|
actor:,
|
|
40
|
-
authorization_context
|
|
41
|
-
|
|
37
|
+
authorization_context:,
|
|
38
|
+
component_key: registration.component_key
|
|
39
|
+
)
|
|
42
40
|
)
|
|
43
41
|
rescue ActionView::MissingTemplate
|
|
44
42
|
raise UnknownComponent,
|
|
45
|
-
"unknown component #{component_name.inspect} for #{snapshot.reference.actor_type}"
|
|
43
|
+
"unknown component #{registration.component_name.inspect} for #{snapshot.reference.actor_type}"
|
|
46
44
|
rescue ActionView::Template::Error => error
|
|
47
45
|
raise error.cause if error.cause.is_a?(SolidObjects::Error)
|
|
48
46
|
|
|
@@ -52,8 +50,7 @@ module SolidObjects
|
|
|
52
50
|
private
|
|
53
51
|
|
|
54
52
|
attr_reader :snapshot,
|
|
55
|
-
:
|
|
56
|
-
:dependencies,
|
|
53
|
+
:registration,
|
|
57
54
|
:view_context,
|
|
58
55
|
:authorization_context
|
|
59
56
|
|
|
@@ -63,7 +60,7 @@ module SolidObjects
|
|
|
63
60
|
actor_type: snapshot.reference.actor_type,
|
|
64
61
|
actor_id: snapshot.reference.actor_id,
|
|
65
62
|
message_name: observable_name,
|
|
66
|
-
arguments:
|
|
63
|
+
arguments: registration.authorization_arguments,
|
|
67
64
|
authorization_context:
|
|
68
65
|
)
|
|
69
66
|
return if authorized
|
|
@@ -76,7 +73,7 @@ module SolidObjects
|
|
|
76
73
|
actor_name = snapshot.actor_class.name
|
|
77
74
|
raise InvalidActor, "anonymous actors cannot render reactive components" unless actor_name
|
|
78
75
|
|
|
79
|
-
"actors/#{actor_name.underscore}/#{component_name}"
|
|
76
|
+
"actors/#{actor_name.underscore}/#{registration.component_name}"
|
|
80
77
|
end
|
|
81
78
|
end
|
|
82
79
|
end
|
|
@@ -28,7 +28,7 @@ module SolidObjects
|
|
|
28
28
|
validate_identity!(registration, reference)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
|
-
if registrations.map(&:
|
|
31
|
+
if registrations.map(&:dom_id).uniq.length != registrations.length
|
|
32
32
|
raise InvalidComponentToken, "duplicate actor component registration"
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -39,7 +39,7 @@ module SolidObjects
|
|
|
39
39
|
def initialize(registrations)
|
|
40
40
|
@registrations = registrations
|
|
41
41
|
@revisions = registrations.to_h do |registration|
|
|
42
|
-
[ registration.
|
|
42
|
+
[ registration.dom_id, registration.revision_key ]
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -51,7 +51,7 @@ module SolidObjects
|
|
|
51
51
|
registrations.filter_map do |registration|
|
|
52
52
|
next unless registration.dependencies.include?(observable_name)
|
|
53
53
|
next unless newer_revision?(
|
|
54
|
-
registration.
|
|
54
|
+
registration.dom_id,
|
|
55
55
|
instance_id,
|
|
56
56
|
revision
|
|
57
57
|
)
|
|
@@ -64,7 +64,7 @@ module SolidObjects
|
|
|
64
64
|
def reconnect_refreshes(snapshot)
|
|
65
65
|
registrations.filter_map do |registration|
|
|
66
66
|
next unless newer_revision?(
|
|
67
|
-
registration.
|
|
67
|
+
registration.dom_id,
|
|
68
68
|
snapshot.instance_id,
|
|
69
69
|
snapshot.revision
|
|
70
70
|
)
|
|
@@ -92,7 +92,7 @@ module SolidObjects
|
|
|
92
92
|
|
|
93
93
|
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
94
94
|
def refresh(registration, instance_id, revision)
|
|
95
|
-
revisions[registration.
|
|
95
|
+
revisions[registration.dom_id] = [ instance_id, revision ]
|
|
96
96
|
TurboStreamRenderer.component_refresh(
|
|
97
97
|
registration,
|
|
98
98
|
instance_id,
|
|
@@ -101,8 +101,8 @@ module SolidObjects
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# @rbs (String, Integer, Integer) -> bool
|
|
104
|
-
def newer_revision?(
|
|
105
|
-
current = revisions.fetch(
|
|
104
|
+
def newer_revision?(dom_id, instance_id, revision)
|
|
105
|
+
current = revisions.fetch(dom_id)
|
|
106
106
|
(current <=> [ instance_id, revision ]) == -1
|
|
107
107
|
end
|
|
108
108
|
end
|
|
@@ -7,23 +7,38 @@ module SolidObjects
|
|
|
7
7
|
PURPOSE = "solid_objects.actor_component"
|
|
8
8
|
MAXIMUM_TOKEN_BYTES = 16_384
|
|
9
9
|
MAXIMUM_DEPENDENCIES = 50
|
|
10
|
+
MAXIMUM_LOCALS = 50
|
|
11
|
+
MAXIMUM_COMPONENT_KEY_BYTES = 512
|
|
12
|
+
REFRESH_METHODS = %w[replace morph].freeze
|
|
13
|
+
RESERVED_LOCALS = %w[actor authorization_context component_key].freeze
|
|
14
|
+
RUBY_KEYWORDS = %w[
|
|
15
|
+
alias and begin break case class def defined do else elsif end ensure
|
|
16
|
+
false for if in module next nil not or redo rescue retry return self
|
|
17
|
+
super then true undef unless until when while yield
|
|
18
|
+
].freeze
|
|
10
19
|
|
|
11
20
|
module_function
|
|
12
21
|
|
|
13
|
-
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String) -> String
|
|
22
|
+
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol) -> String
|
|
14
23
|
def generate(
|
|
15
24
|
reference:,
|
|
16
25
|
component_name:,
|
|
17
26
|
dependencies:,
|
|
18
27
|
instance_id:,
|
|
19
28
|
revision:,
|
|
20
|
-
refresh_path
|
|
29
|
+
refresh_path:,
|
|
30
|
+
component_key: nil,
|
|
31
|
+
locals: {},
|
|
32
|
+
refresh_method: "replace"
|
|
21
33
|
)
|
|
22
34
|
payload = {
|
|
23
35
|
"actor_type" => reference.actor_type,
|
|
24
36
|
"actor_id" => reference.actor_id,
|
|
25
37
|
"component_name" => component_name,
|
|
38
|
+
"component_key" => Serialization.dump(component_key),
|
|
26
39
|
"dependencies" => dependencies,
|
|
40
|
+
"locals" => Serialization.dump(locals),
|
|
41
|
+
"refresh_method" => refresh_method.to_s,
|
|
27
42
|
"instance_id" => instance_id,
|
|
28
43
|
"revision" => revision,
|
|
29
44
|
"refresh_path" => refresh_path
|
|
@@ -41,6 +56,7 @@ module SolidObjects
|
|
|
41
56
|
end
|
|
42
57
|
|
|
43
58
|
payload = verifier.verified(token, purpose: PURPOSE)
|
|
59
|
+
apply_defaults!(payload)
|
|
44
60
|
validate_payload!(payload)
|
|
45
61
|
payload
|
|
46
62
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
@@ -54,6 +70,8 @@ module SolidObjects
|
|
|
54
70
|
payload["actor_id"].is_a?(String) &&
|
|
55
71
|
payload["component_name"].is_a?(String) &&
|
|
56
72
|
payload["dependencies"].is_a?(Array) &&
|
|
73
|
+
payload["locals"].is_a?(Hash) &&
|
|
74
|
+
payload["refresh_method"].is_a?(String) &&
|
|
57
75
|
payload["instance_id"].is_a?(Integer) &&
|
|
58
76
|
payload["revision"].is_a?(Integer) &&
|
|
59
77
|
payload["refresh_path"].is_a?(String)
|
|
@@ -61,7 +79,10 @@ module SolidObjects
|
|
|
61
79
|
end
|
|
62
80
|
|
|
63
81
|
validate_component_name!(payload.fetch("component_name"))
|
|
82
|
+
validate_component_key!(payload["component_key"])
|
|
64
83
|
validate_dependencies!(payload.fetch("dependencies"))
|
|
84
|
+
validate_locals!(payload.fetch("locals"))
|
|
85
|
+
validate_refresh_method!(payload.fetch("refresh_method"))
|
|
65
86
|
validate_revision!(
|
|
66
87
|
payload.fetch("instance_id"),
|
|
67
88
|
payload.fetch("revision")
|
|
@@ -71,6 +92,16 @@ module SolidObjects
|
|
|
71
92
|
end
|
|
72
93
|
private_class_method :validate_payload!
|
|
73
94
|
|
|
95
|
+
# @rbs (Hash[String, untyped]?) -> void
|
|
96
|
+
def apply_defaults!(payload)
|
|
97
|
+
return unless payload.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
payload["component_key"] = nil unless payload.key?("component_key")
|
|
100
|
+
payload["locals"] = {} unless payload.key?("locals")
|
|
101
|
+
payload["refresh_method"] = "replace" unless payload.key?("refresh_method")
|
|
102
|
+
end
|
|
103
|
+
private_class_method :apply_defaults!
|
|
104
|
+
|
|
74
105
|
# @rbs (String) -> void
|
|
75
106
|
def validate_component_name!(component_name)
|
|
76
107
|
return if component_name.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
@@ -79,6 +110,20 @@ module SolidObjects
|
|
|
79
110
|
end
|
|
80
111
|
private_class_method :validate_component_name!
|
|
81
112
|
|
|
113
|
+
# @rbs (untyped) -> void
|
|
114
|
+
def validate_component_key!(component_key)
|
|
115
|
+
return if component_key.nil?
|
|
116
|
+
return if component_key.is_a?(Integer)
|
|
117
|
+
if component_key.is_a?(String) &&
|
|
118
|
+
component_key.bytesize.positive? &&
|
|
119
|
+
component_key.bytesize <= MAXIMUM_COMPONENT_KEY_BYTES
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
raise InvalidComponentToken, "invalid actor component key"
|
|
124
|
+
end
|
|
125
|
+
private_class_method :validate_component_key!
|
|
126
|
+
|
|
82
127
|
# @rbs (Array[untyped]) -> void
|
|
83
128
|
def validate_dependencies!(dependencies)
|
|
84
129
|
valid = dependencies.any? &&
|
|
@@ -91,6 +136,29 @@ module SolidObjects
|
|
|
91
136
|
end
|
|
92
137
|
private_class_method :validate_dependencies!
|
|
93
138
|
|
|
139
|
+
# @rbs (Hash[untyped, untyped]) -> void
|
|
140
|
+
def validate_locals!(locals)
|
|
141
|
+
valid = locals.length <= MAXIMUM_LOCALS &&
|
|
142
|
+
locals.keys.all? do |name|
|
|
143
|
+
name.is_a?(String) &&
|
|
144
|
+
name.match?(/\A[a-z_][a-zA-Z0-9_]*\z/) &&
|
|
145
|
+
!RESERVED_LOCALS.include?(name) &&
|
|
146
|
+
!RUBY_KEYWORDS.include?(name)
|
|
147
|
+
end
|
|
148
|
+
return if valid
|
|
149
|
+
|
|
150
|
+
raise InvalidComponentToken, "invalid actor component locals"
|
|
151
|
+
end
|
|
152
|
+
private_class_method :validate_locals!
|
|
153
|
+
|
|
154
|
+
# @rbs (String) -> void
|
|
155
|
+
def validate_refresh_method!(refresh_method)
|
|
156
|
+
return if REFRESH_METHODS.include?(refresh_method)
|
|
157
|
+
|
|
158
|
+
raise InvalidComponentToken, "invalid actor component refresh method"
|
|
159
|
+
end
|
|
160
|
+
private_class_method :validate_refresh_method!
|
|
161
|
+
|
|
94
162
|
# @rbs (Integer, Integer) -> void
|
|
95
163
|
def validate_revision!(instance_id, revision)
|
|
96
164
|
return unless instance_id.negative? || revision.negative?
|
|
@@ -16,9 +16,12 @@ module SolidObjects
|
|
|
16
16
|
"#{scope(reference)}_observable_#{normalized_name(name)}"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# @rbs (Reference, Symbol | String) -> String
|
|
20
|
-
def component(reference, name)
|
|
21
|
-
"#{scope(reference)}_component_#{normalized_name(name)}"
|
|
19
|
+
# @rbs (Reference, Symbol | String, ?key: String | Integer?) -> String
|
|
20
|
+
def component(reference, name, key: nil)
|
|
21
|
+
identity = "#{scope(reference)}_component_#{normalized_name(name)}"
|
|
22
|
+
return identity unless key
|
|
23
|
+
|
|
24
|
+
"#{identity}_#{component_key_digest(key)}"
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# @rbs (Reference) -> String
|
|
@@ -34,5 +37,11 @@ module SolidObjects
|
|
|
34
37
|
name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
35
38
|
end
|
|
36
39
|
private_class_method :normalized_name
|
|
40
|
+
|
|
41
|
+
# @rbs (String | Integer) -> String
|
|
42
|
+
def component_key_digest(key)
|
|
43
|
+
Digest::SHA256.hexdigest(JSON.generate(key)).first(24)
|
|
44
|
+
end
|
|
45
|
+
private_class_method :component_key_digest
|
|
37
46
|
end
|
|
38
47
|
end
|
data/lib/solid_objects/engine.rb
CHANGED
|
@@ -26,6 +26,13 @@ module SolidObjects
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
initializer "solid_objects.assets" do |application|
|
|
30
|
+
next unless application.config.respond_to?(:assets)
|
|
31
|
+
next unless application.config.assets.respond_to?(:precompile)
|
|
32
|
+
|
|
33
|
+
application.config.assets.precompile << "solid_objects/component_refresh.js"
|
|
34
|
+
end
|
|
35
|
+
|
|
29
36
|
rake_tasks do
|
|
30
37
|
tasks_path = File.expand_path("../tasks", __dir__)
|
|
31
38
|
Dir[File.join(tasks_path, "**/*.rake")].sort.each { |task| load task }
|
|
@@ -40,15 +40,14 @@ module SolidObjects
|
|
|
40
40
|
|
|
41
41
|
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
42
42
|
def component_refresh(registration, instance_id, revision)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
43
|
+
return morph_component_refresh(registration, instance_id, revision) if registration.morph?
|
|
44
|
+
|
|
45
|
+
target = registration.dom_id
|
|
47
46
|
source = ERB::Util.html_escape(
|
|
48
47
|
registration.refresh_url(instance_id, revision)
|
|
49
48
|
)
|
|
50
49
|
revision_value = "#{instance_id}:#{revision}"
|
|
51
|
-
%(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}"></turbo-frame></template></turbo-stream>)
|
|
50
|
+
%(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}" data-solid-objects-refresh="replace"></turbo-frame></template></turbo-stream>)
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
# @rbs (String) -> Hash[String, untyped]?
|
|
@@ -75,5 +74,15 @@ module SolidObjects
|
|
|
75
74
|
JSON.generate(value)
|
|
76
75
|
end
|
|
77
76
|
private_class_method :display_value
|
|
77
|
+
|
|
78
|
+
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
79
|
+
def morph_component_refresh(registration, instance_id, revision)
|
|
80
|
+
source = ERB::Util.html_escape(
|
|
81
|
+
registration.refresh_url(instance_id, revision)
|
|
82
|
+
)
|
|
83
|
+
scope = DomIdentity.scope(registration.reference)
|
|
84
|
+
%(<turbo-stream action="append" target="#{scope}"><template><solid-objects-refresh data-target="#{registration.dom_id}" data-source="#{source}" data-refresh-method="morph"></solid-objects-refresh></template></turbo-stream>)
|
|
85
|
+
end
|
|
86
|
+
private_class_method :morph_component_refresh
|
|
78
87
|
end
|
|
79
88
|
end
|
|
@@ -22,8 +22,8 @@ module SolidObjects
|
|
|
22
22
|
# @rbs (Symbol | String) -> untyped
|
|
23
23
|
def value: (Symbol | String) -> untyped
|
|
24
24
|
|
|
25
|
-
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?) -> untyped
|
|
26
|
-
def component: (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?) -> untyped
|
|
25
|
+
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
|
|
26
|
+
def component: (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
|
|
27
27
|
|
|
28
28
|
# @rbs () -> Array[String]
|
|
29
29
|
def component_tokens: () -> Array[String]
|
|
@@ -31,6 +31,9 @@ module SolidObjects
|
|
|
31
31
|
# @rbs () -> Array[String]
|
|
32
32
|
def scalar_observable_names: () -> Array[String]
|
|
33
33
|
|
|
34
|
+
# @rbs () -> bool
|
|
35
|
+
def morph_components?: () -> bool
|
|
36
|
+
|
|
34
37
|
# @rbs () -> State
|
|
35
38
|
def state: () -> State
|
|
36
39
|
|
|
@@ -73,8 +76,11 @@ module SolidObjects
|
|
|
73
76
|
# @rbs (Array[String]) -> void
|
|
74
77
|
def validate_dependencies!: (Array[String]) -> void
|
|
75
78
|
|
|
76
|
-
# @rbs (
|
|
77
|
-
def ensure_unique_component!: (
|
|
79
|
+
# @rbs (ComponentRegistration) -> void
|
|
80
|
+
def ensure_unique_component!: (ComponentRegistration) -> void
|
|
81
|
+
|
|
82
|
+
# @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
|
|
83
|
+
def validate_static_options!: (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
|
|
78
84
|
|
|
79
85
|
# @rbs () -> Proc | ComponentPathResolver
|
|
80
86
|
def component_path_resolver: () -> Proc
|