interfacets 0.1.0 → 0.9.99
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/.rubocop.yml +173 -1
- data/.tmp +5 -0
- data/LICENSE +21 -0
- data/Rakefile +9 -7
- data/lib/interfacets/client/actor.rb +20 -0
- data/lib/interfacets/client/assets.rb +210 -0
- data/lib/interfacets/client/bus.rb +73 -0
- data/lib/interfacets/client/channels/api.rb +102 -0
- data/lib/interfacets/client/channels/audio.rb +101 -0
- data/lib/interfacets/client/channels/base.rb +28 -0
- data/lib/interfacets/client/channels/page_visibility.rb +21 -0
- data/lib/interfacets/client/channels/react/builder.rb +203 -0
- data/lib/interfacets/client/channels/react/channel.rb +61 -0
- data/lib/interfacets/client/channels/react/dom.rb +91 -0
- data/lib/interfacets/client/channels/react/evaluator.rb +33 -0
- data/lib/interfacets/client/channels/speech_to_text.rb +100 -0
- data/lib/interfacets/client/channels/timer.rb +51 -0
- data/lib/interfacets/client/channels/url.rb +52 -0
- data/lib/interfacets/client/config.rb +22 -0
- data/lib/interfacets/client/delegator.rb +37 -0
- data/lib/interfacets/client/registry.rb +23 -0
- data/lib/interfacets/client/system.rb +88 -0
- data/lib/interfacets/client/utils/active_support_concern.rb +220 -0
- data/lib/interfacets/client/utils/mruby_patches.rb +81 -0
- data/lib/interfacets/client/utils/open_struct.rb +115 -0
- data/lib/interfacets/client/utils/securerandom.rb +69 -0
- data/lib/interfacets/client.rb +13 -0
- data/lib/interfacets/component_registry.rb +115 -0
- data/lib/interfacets/component_schema_parser.rb +84 -0
- data/lib/interfacets/mruby/build.dockerfile +66 -0
- data/lib/interfacets/mruby/build_config.rb +20 -0
- data/lib/interfacets/mruby/entrypoint.rb +23 -0
- data/lib/interfacets/mruby/init.c +66 -0
- data/lib/interfacets/server/api.rb +44 -0
- data/lib/interfacets/server/assets/facet.rb +63 -0
- data/lib/interfacets/server/assets.rb +216 -0
- data/lib/interfacets/server/basic_router.rb +79 -0
- data/lib/interfacets/server/bus.rb +34 -0
- data/lib/interfacets/server/config.rb +87 -0
- data/lib/interfacets/server/facets/deserializer.rb +25 -0
- data/lib/interfacets/server/facets/schema/serializer.rb +54 -0
- data/lib/interfacets/server/facets/serializer.rb +50 -0
- data/lib/interfacets/server/registry.rb +51 -0
- data/lib/interfacets/shared/basic_routable.rb +45 -0
- data/lib/interfacets/shared/entities/bus.rb +230 -0
- data/lib/interfacets/shared/entities/collection_proxy.rb +190 -0
- data/lib/interfacets/shared/entities/specs/handlers.rb +133 -0
- data/lib/interfacets/shared/entities/specs.rb +161 -0
- data/lib/interfacets/shared/entity.rb +102 -0
- data/lib/interfacets/shared/entity_dsl.rb +154 -0
- data/lib/interfacets/shared/facet.rb +200 -0
- data/lib/interfacets/shared/generated_store.rb +149 -0
- data/lib/interfacets/shared/utils.rb +54 -0
- data/lib/interfacets/shared/validations.rb +75 -0
- data/lib/interfacets/shared/view.rb +74 -0
- data/lib/interfacets/test/component_registry.rb +63 -0
- data/lib/interfacets/test/js/inline_bus.rb +100 -0
- data/lib/interfacets/test/js/nodo_bus.rb +98 -0
- data/lib/interfacets/test/js/receivers/api.rb +48 -0
- data/lib/interfacets/test/js/receivers/react/node/xml_parser.rb +75 -0
- data/lib/interfacets/test/js/receivers/react/node.rb +133 -0
- data/lib/interfacets/test/js/receivers/react.rb +32 -0
- data/lib/interfacets/test/js/receivers/timer.rb +77 -0
- data/lib/interfacets/test/js/receivers/url.rb +60 -0
- data/lib/interfacets/test/standard_elements.yml +173 -0
- data/lib/interfacets/test/ui_simulator.rb +75 -0
- data/lib/interfacets/test/validation_engine.rb +151 -0
- data/lib/interfacets/test.rb +13 -0
- data/lib/interfacets/version.rb +1 -1
- data/lib/interfacets.rb +29 -2
- metadata +114 -6
- data/README.md +0 -35
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interfacets
|
|
4
|
+
module Shared
|
|
5
|
+
module Entities
|
|
6
|
+
class Bus
|
|
7
|
+
InvalidAction = Class.new(StandardError)
|
|
8
|
+
InvalidReceiver = Class.new(StandardError)
|
|
9
|
+
|
|
10
|
+
# event schema:
|
|
11
|
+
# {
|
|
12
|
+
# from: (role),
|
|
13
|
+
# to: (role),
|
|
14
|
+
# action:,
|
|
15
|
+
# payload: {
|
|
16
|
+
|
|
17
|
+
# }
|
|
18
|
+
# }
|
|
19
|
+
|
|
20
|
+
attr_reader :manifest, :entity
|
|
21
|
+
def initialize(entity:)
|
|
22
|
+
@manifest = entity.class.manifest
|
|
23
|
+
@entity = entity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def serialize(to:, action:, nesting:)
|
|
27
|
+
assert_valid_action(action:, to:, nesting:)
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
id: SecureRandom.uuid,
|
|
31
|
+
from: entity.role,
|
|
32
|
+
to: to,
|
|
33
|
+
nesting:,
|
|
34
|
+
action:,
|
|
35
|
+
payload: {
|
|
36
|
+
attributes: serialize_attributes(manifest:, entity:, to:)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def handle(event:)
|
|
42
|
+
action = event.fetch("action")
|
|
43
|
+
|
|
44
|
+
assert_valid_receiver(to: event.fetch("to"))
|
|
45
|
+
assert_valid_action(
|
|
46
|
+
action:,
|
|
47
|
+
to: entity.role,
|
|
48
|
+
nesting: event.fetch("nesting"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
attributes = event.fetch("payload").fetch("attributes", {})
|
|
52
|
+
|
|
53
|
+
merge(entity:, manifest:, attributes:, action:)
|
|
54
|
+
|
|
55
|
+
target_entity = entity.entity_at(event.fetch("nesting"))
|
|
56
|
+
|
|
57
|
+
target_entity
|
|
58
|
+
&.class
|
|
59
|
+
&.actions
|
|
60
|
+
&.fetch(event.fetch("action"))
|
|
61
|
+
&.dispatch(target_entity)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def serialize_attributes(manifest:, entity:, to:)
|
|
67
|
+
return if entity.nil?
|
|
68
|
+
|
|
69
|
+
data = {}
|
|
70
|
+
|
|
71
|
+
data[:errors] = entity.errors.to_h if entity.respond_to?(:errors)
|
|
72
|
+
|
|
73
|
+
manifest
|
|
74
|
+
.accessors
|
|
75
|
+
.each do |name, spec|
|
|
76
|
+
data[name] = entity.send(name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
manifest
|
|
80
|
+
.associations
|
|
81
|
+
.each do |name, spec|
|
|
82
|
+
if spec.type == :reference
|
|
83
|
+
data[name] = serialize_attributes(
|
|
84
|
+
manifest: spec.klass,
|
|
85
|
+
entity: entity.send(name),
|
|
86
|
+
to:
|
|
87
|
+
)
|
|
88
|
+
else
|
|
89
|
+
data[name] = (entity.send(name) || []).map {
|
|
90
|
+
serialize_attributes(
|
|
91
|
+
manifest: spec.klass,
|
|
92
|
+
entity: _1,
|
|
93
|
+
to:
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
data
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def merge(manifest:, entity:, attributes:, action:)
|
|
103
|
+
if (errs = attributes[:errors] || attributes["errors"])
|
|
104
|
+
entity.errors.clear if entity.errors.respond_to?(:clear)
|
|
105
|
+
errs.each do |k, vs|
|
|
106
|
+
Array(vs).each { |v| entity.errors.add(k.to_sym, v) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
manifest
|
|
111
|
+
.accessors
|
|
112
|
+
.values
|
|
113
|
+
.select { _1.accepted_by?(entity.role) }
|
|
114
|
+
.each do |attribute|
|
|
115
|
+
next unless attributes.key?(attribute.name)
|
|
116
|
+
|
|
117
|
+
attr_mergers = (
|
|
118
|
+
entity.class.mergers[attribute.name]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
merger = (
|
|
122
|
+
if attr_mergers.key?(action)
|
|
123
|
+
attr_mergers.fetch(action)
|
|
124
|
+
else
|
|
125
|
+
attr_mergers.fetch(:default)
|
|
126
|
+
end
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
merger.call(entity, attributes[attribute.name])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
manifest
|
|
133
|
+
.associations
|
|
134
|
+
.values
|
|
135
|
+
.select { _1.type == :reference }
|
|
136
|
+
.select { _1.accepted_by?(entity.role) }
|
|
137
|
+
.each do |association|
|
|
138
|
+
next unless attributes.key?(association.name)
|
|
139
|
+
|
|
140
|
+
value = attributes.fetch(association.name)
|
|
141
|
+
|
|
142
|
+
if value.nil?
|
|
143
|
+
entity.association(association.name).set(nil)
|
|
144
|
+
next
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
entity
|
|
148
|
+
.association(association.name)
|
|
149
|
+
.get
|
|
150
|
+
.then { _1 || entity.association(association.name).build }
|
|
151
|
+
.tap { |nested_entity|
|
|
152
|
+
merge(
|
|
153
|
+
entity: nested_entity,
|
|
154
|
+
manifest: association.klass,
|
|
155
|
+
attributes: value,
|
|
156
|
+
action:
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
.then { entity.association(association.name).set(_1) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
manifest
|
|
163
|
+
.associations
|
|
164
|
+
.values
|
|
165
|
+
.select { _1.type == :collection }
|
|
166
|
+
.select { _1.accepted_by?(entity.role) }
|
|
167
|
+
.each do |collection|
|
|
168
|
+
next unless attributes.key?(collection.name)
|
|
169
|
+
|
|
170
|
+
identifier = collection.identifier
|
|
171
|
+
|
|
172
|
+
incoming_coll = attributes.fetch(collection.name) || []
|
|
173
|
+
existing_coll = entity.send(collection.name) || []
|
|
174
|
+
|
|
175
|
+
items = (
|
|
176
|
+
incoming_coll.map do |val|
|
|
177
|
+
found_item = (
|
|
178
|
+
val.fetch(identifier) &&
|
|
179
|
+
existing_coll.find { _1.send(identifier) == val.fetch(identifier) }
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if found_item
|
|
183
|
+
found_item.tap { |r|
|
|
184
|
+
merge(
|
|
185
|
+
entity: r,
|
|
186
|
+
manifest: collection.klass,
|
|
187
|
+
attributes: val,
|
|
188
|
+
action:
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
else
|
|
192
|
+
entity
|
|
193
|
+
.association(collection.name)
|
|
194
|
+
.build
|
|
195
|
+
.tap { |r|
|
|
196
|
+
merge(
|
|
197
|
+
entity: r,
|
|
198
|
+
manifest: collection.klass,
|
|
199
|
+
attributes: val,
|
|
200
|
+
action:
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
)
|
|
206
|
+
entity.association(collection.name).set(items)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def assert_valid_action(action:, to:, nesting:)
|
|
211
|
+
nested_manifest = manifest
|
|
212
|
+
|
|
213
|
+
nesting[1..-1].each do |assoc_name, _id|
|
|
214
|
+
nested_manifest = manifest.associations.fetch(assoc_name).klass
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
unless nested_manifest.actions[action]&.accepted_by?(to.to_s)
|
|
218
|
+
raise InvalidAction.new("invalid action: #{action}")
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def assert_valid_receiver(to:)
|
|
223
|
+
if to != entity.role
|
|
224
|
+
raise InvalidReceiver.new("invalid receiver role: to: #{to}, entity: #{entity.role.inspect}")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interfacets
|
|
4
|
+
module Shared
|
|
5
|
+
module Entities
|
|
6
|
+
class CollectionProxy
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :wrap, :unwrap
|
|
10
|
+
def initialize(collection, wrap:, unwrap:)
|
|
11
|
+
@collection = collection
|
|
12
|
+
@wrap = wrap
|
|
13
|
+
@unwrap = unwrap
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Read operations - wrap items when accessing
|
|
17
|
+
def first
|
|
18
|
+
val = @collection.first
|
|
19
|
+
val.nil? ? nil : @wrap.call(val)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def last
|
|
23
|
+
val = @collection.last
|
|
24
|
+
val.nil? ? nil : @wrap.call(val)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def [](index)
|
|
28
|
+
val = @collection[index]
|
|
29
|
+
val.nil? ? nil : @wrap.call(val)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def at(index)
|
|
33
|
+
val = @collection.at(index)
|
|
34
|
+
val.nil? ? nil : @wrap.call(val)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def each(&block)
|
|
38
|
+
@collection.each { |item| block.call(@wrap.call(item)) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def size
|
|
42
|
+
@collection.size
|
|
43
|
+
end
|
|
44
|
+
alias_method :length, :size
|
|
45
|
+
alias_method :count, :size
|
|
46
|
+
|
|
47
|
+
def empty?
|
|
48
|
+
@collection.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Mutation operations - unwrap items and delegate to underlying collection
|
|
52
|
+
def <<(item)
|
|
53
|
+
@collection << @unwrap.call(item)
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def push(*items)
|
|
58
|
+
@collection.push(*items.map { |item| @unwrap.call(item) })
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def pop
|
|
63
|
+
val = @collection.pop
|
|
64
|
+
val.nil? ? nil : @wrap.call(val)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def shift
|
|
68
|
+
val = @collection.shift
|
|
69
|
+
val.nil? ? nil : @wrap.call(val)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def unshift(*items)
|
|
73
|
+
@collection.unshift(*items.map { |item| @unwrap.call(item) })
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def []=(index, item)
|
|
78
|
+
@collection[index] = @unwrap.call(item)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def delete(item)
|
|
82
|
+
unwrapped = @unwrap.call(item)
|
|
83
|
+
val = @collection.delete(unwrapped)
|
|
84
|
+
val.nil? ? nil : @wrap.call(val)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def delete_at(index)
|
|
88
|
+
val = @collection.delete_at(index)
|
|
89
|
+
val.nil? ? nil : @wrap.call(val)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def clear
|
|
93
|
+
@collection.clear
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def insert(index, *items)
|
|
98
|
+
@collection.insert(index, *items.map { |item| @unwrap.call(item) })
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def concat(other_array)
|
|
103
|
+
@collection.concat(other_array.map { |item| @unwrap.call(item) })
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def replace(other_array)
|
|
108
|
+
@collection.replace(other_array.map { |item| @unwrap.call(item) })
|
|
109
|
+
self
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Enumerable methods that return arrays should return CollectionProxy
|
|
113
|
+
def select(&block)
|
|
114
|
+
selected = @collection.select { |item| block.call(@wrap.call(item)) }
|
|
115
|
+
CollectionProxy.new(selected, wrap: @wrap, unwrap: @unwrap)
|
|
116
|
+
end
|
|
117
|
+
alias_method :filter, :select
|
|
118
|
+
|
|
119
|
+
def reject(&block)
|
|
120
|
+
rejected = @collection.reject { |item| block.call(@wrap.call(item)) }
|
|
121
|
+
CollectionProxy.new(rejected, wrap: @wrap, unwrap: @unwrap)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def take(n)
|
|
125
|
+
taken = @collection.take(n)
|
|
126
|
+
CollectionProxy.new(taken, wrap: @wrap, unwrap: @unwrap)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def take_while(&block)
|
|
130
|
+
taken = @collection.take_while { |item| block.call(@wrap.call(item)) }
|
|
131
|
+
CollectionProxy.new(taken, wrap: @wrap, unwrap: @unwrap)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def drop(n)
|
|
135
|
+
dropped = @collection.drop(n)
|
|
136
|
+
CollectionProxy.new(dropped, wrap: @wrap, unwrap: @unwrap)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def drop_while(&block)
|
|
140
|
+
dropped = @collection.drop_while { |item| block.call(@wrap.call(item)) }
|
|
141
|
+
CollectionProxy.new(dropped, wrap: @wrap, unwrap: @unwrap)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def reverse
|
|
145
|
+
reversed = @collection.reverse
|
|
146
|
+
CollectionProxy.new(reversed, wrap: @wrap, unwrap: @unwrap)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def sort(&block)
|
|
150
|
+
if block
|
|
151
|
+
sorted = @collection.sort { |a, b| block.call(@wrap.call(a), @wrap.call(b)) }
|
|
152
|
+
else
|
|
153
|
+
sorted = @collection.sort
|
|
154
|
+
end
|
|
155
|
+
CollectionProxy.new(sorted, wrap: @wrap, unwrap: @unwrap)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def sort_by(&block)
|
|
159
|
+
sorted = @collection.sort_by { |item| block.call(@wrap.call(item)) }
|
|
160
|
+
CollectionProxy.new(sorted, wrap: @wrap, unwrap: @unwrap)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def uniq(&block)
|
|
164
|
+
if block
|
|
165
|
+
uniqued = @collection.uniq { |item| block.call(@wrap.call(item)) }
|
|
166
|
+
else
|
|
167
|
+
uniqued = @collection.uniq
|
|
168
|
+
end
|
|
169
|
+
CollectionProxy.new(uniqued, wrap: @wrap, unwrap: @unwrap)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def compact
|
|
173
|
+
compacted = @collection.compact
|
|
174
|
+
CollectionProxy.new(compacted, wrap: @wrap, unwrap: @unwrap)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def slice(*args)
|
|
178
|
+
sliced = @collection.slice(*args)
|
|
179
|
+
if sliced.is_a?(Array)
|
|
180
|
+
CollectionProxy.new(sliced, wrap: @wrap, unwrap: @unwrap)
|
|
181
|
+
else
|
|
182
|
+
# Single element access returns wrapped item
|
|
183
|
+
sliced.nil? ? nil : @wrap.call(sliced)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interfacets
|
|
4
|
+
module Shared
|
|
5
|
+
module Entities
|
|
6
|
+
module Specs
|
|
7
|
+
module Handlers
|
|
8
|
+
class Handler
|
|
9
|
+
attr_reader :entity, :spec, :cache
|
|
10
|
+
def initialize(entity:, spec:)
|
|
11
|
+
@entity = entity
|
|
12
|
+
@spec = spec
|
|
13
|
+
@cache = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def type
|
|
17
|
+
spec.type
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unwrap(value)
|
|
21
|
+
if value.is_a?(Entity)
|
|
22
|
+
value.store
|
|
23
|
+
elsif value.is_a?(Array)
|
|
24
|
+
value.map { unwrap(_1) }
|
|
25
|
+
else
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def wrap(value)
|
|
31
|
+
if value.is_a?(Entity)
|
|
32
|
+
value
|
|
33
|
+
elsif cache[value]
|
|
34
|
+
cache[value]
|
|
35
|
+
elsif value
|
|
36
|
+
@cache[value] = spec.klass.new(
|
|
37
|
+
store: value,
|
|
38
|
+
nesting: spec.name,
|
|
39
|
+
parent: entity,
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Reference < Handler
|
|
46
|
+
def collection?
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reference?
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def get
|
|
55
|
+
wrap(entity.instance_exec(&spec.getter))
|
|
56
|
+
.tap {
|
|
57
|
+
@cache = {}
|
|
58
|
+
@cache[_1&.store] = _1
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set(val)
|
|
63
|
+
entity.instance_exec(unwrap(val), &spec.setter)
|
|
64
|
+
wrap(val)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build(**attributes)
|
|
68
|
+
value = entity.instance_exec(&spec.builder)
|
|
69
|
+
attributes.each do |name, val|
|
|
70
|
+
value.send("#{name}=", val)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# TODO: make sure this is right
|
|
74
|
+
# set(value)
|
|
75
|
+
|
|
76
|
+
wrap(value)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class Collection < Handler
|
|
81
|
+
def collection?
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def reference?
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get
|
|
90
|
+
values = entity.instance_exec(&spec.getter) || []
|
|
91
|
+
|
|
92
|
+
CollectionProxy.new(
|
|
93
|
+
values,
|
|
94
|
+
wrap: ->(val) { wrap(val) },
|
|
95
|
+
unwrap: ->(val) { unwrap(val) },
|
|
96
|
+
).tap { |entities|
|
|
97
|
+
@cache = entities.map { [_1.store, _1] }.to_h
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def set(val)
|
|
102
|
+
@cache ||= {}
|
|
103
|
+
val.each do |item|
|
|
104
|
+
if item.is_a?(Entity)
|
|
105
|
+
@cache[item.store] = item
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
entity.instance_exec(unwrap(val), &spec.setter)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build(**attributes)
|
|
113
|
+
value = unwrap(entity.instance_exec(&spec.builder))
|
|
114
|
+
attributes.each do |name, val|
|
|
115
|
+
value.send("#{name}=", val)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
items = entity.instance_exec(&spec.getter)
|
|
119
|
+
|
|
120
|
+
if items.nil?
|
|
121
|
+
set([value])
|
|
122
|
+
elsif !items.include?(value)
|
|
123
|
+
items << value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
wrap(value)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interfacets
|
|
4
|
+
module Shared
|
|
5
|
+
module Entities
|
|
6
|
+
module Specs
|
|
7
|
+
|
|
8
|
+
# I would like to use Object.new for object equality here, however,
|
|
9
|
+
# in the intergration tests, our shared code gets evaled on the frontend
|
|
10
|
+
# and again on the backend (for now). For that reason, we cannot use
|
|
11
|
+
# object equality because the constant will be overridden.
|
|
12
|
+
NOT_PASSED = :interfacets__not_passed_flag
|
|
13
|
+
ANY = :interfacets__any_flag
|
|
14
|
+
|
|
15
|
+
module Acceptable
|
|
16
|
+
def accepted_by?(role)
|
|
17
|
+
standardized_accepted_by == ANY || standardized_accepted_by.include?(role)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def standardized_accepted_by
|
|
21
|
+
@standardized_accepted_by ||= (
|
|
22
|
+
if accepted_by == ANY
|
|
23
|
+
ANY
|
|
24
|
+
else
|
|
25
|
+
Array(accepted_by).map(&:to_s)
|
|
26
|
+
end
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Action
|
|
32
|
+
include Acceptable
|
|
33
|
+
|
|
34
|
+
attr_reader :name, :accepted_by, :only_if_valid
|
|
35
|
+
def initialize(name:, accepted_by:, only_if_valid:)
|
|
36
|
+
@name = name
|
|
37
|
+
@accepted_by = accepted_by
|
|
38
|
+
@only_if_valid = only_if_valid
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def type = :action
|
|
43
|
+
|
|
44
|
+
def dispatch(entity)
|
|
45
|
+
if !only_if_valid || entity.valid?
|
|
46
|
+
entity.send(name)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Accessor
|
|
52
|
+
include Acceptable
|
|
53
|
+
|
|
54
|
+
attr_reader :name, :accepted_by
|
|
55
|
+
def initialize(name:, accepted_by:)
|
|
56
|
+
@name = name
|
|
57
|
+
@accepted_by = accepted_by
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def type = :accessor
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class Association
|
|
64
|
+
include Acceptable
|
|
65
|
+
|
|
66
|
+
attr_reader(
|
|
67
|
+
:name,
|
|
68
|
+
:accepted_by,
|
|
69
|
+
:klass,
|
|
70
|
+
:getter,
|
|
71
|
+
:setter,
|
|
72
|
+
:builder,
|
|
73
|
+
:parent,
|
|
74
|
+
:identifier,
|
|
75
|
+
:type,
|
|
76
|
+
)
|
|
77
|
+
def initialize(parent:, name:)
|
|
78
|
+
@parent = parent
|
|
79
|
+
@name = name
|
|
80
|
+
@accepted_by = Entities::Specs::ANY
|
|
81
|
+
@getter = -> { store.send(name) }
|
|
82
|
+
@setter = ->(val) { store.send("#{name}=", val) }
|
|
83
|
+
@builder = ->() { store.association(name).build }
|
|
84
|
+
@identifier = "id"
|
|
85
|
+
|
|
86
|
+
@klass = Class.new(Entity) do
|
|
87
|
+
define_singleton_method(:name) { "#{parent.name}.#{name}" }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def apply(
|
|
92
|
+
accepted_by: NOT_PASSED,
|
|
93
|
+
getter: NOT_PASSED,
|
|
94
|
+
setter: NOT_PASSED,
|
|
95
|
+
builder: NOT_PASSED,
|
|
96
|
+
identifier: NOT_PASSED,
|
|
97
|
+
mod: nil,
|
|
98
|
+
type: NOT_PASSED,
|
|
99
|
+
&block
|
|
100
|
+
)
|
|
101
|
+
@accepted_by = Array(accepted_by).map(&:to_s) unless accepted_by == NOT_PASSED
|
|
102
|
+
@getter = getter unless getter == NOT_PASSED
|
|
103
|
+
@setter = setter unless setter == NOT_PASSED
|
|
104
|
+
@builder = builder unless builder == NOT_PASSED
|
|
105
|
+
@identifier = identifier.to_s unless identifier == NOT_PASSED
|
|
106
|
+
|
|
107
|
+
if type && type != NOT_PASSED
|
|
108
|
+
type = type.to_sym
|
|
109
|
+
unless [:reference, :collection].include?(type)
|
|
110
|
+
raise ArgumentError.new("invalid association type: #{type.inspect}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if @type && type != @type
|
|
114
|
+
raise ArgumentError.new("invalid association cannot change type")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@type = type
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
@klass.include(mod) if mod
|
|
121
|
+
|
|
122
|
+
@klass.class_exec(&block) if block_given?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def dup_for(new_parent)
|
|
126
|
+
dup.tap do |new_spec|
|
|
127
|
+
new_spec.instance_variable_set(:@parent, new_parent)
|
|
128
|
+
new_spec.instance_variable_set(:@klass, Class.new(@klass))
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def handler(entity)
|
|
133
|
+
klass = (
|
|
134
|
+
if type == :reference
|
|
135
|
+
Handlers::Reference
|
|
136
|
+
elsif type == :collection
|
|
137
|
+
Handlers::Collection
|
|
138
|
+
else
|
|
139
|
+
raise "Type was never set for association #{parent.name}.#{name}"
|
|
140
|
+
end
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
klass.new(entity:, spec: self)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
class Merger
|
|
148
|
+
attr_reader(:name, :block)
|
|
149
|
+
def initialize(name:, block:)
|
|
150
|
+
@name = name
|
|
151
|
+
@block = block
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def call(entity, value)
|
|
155
|
+
block.call(entity, value)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|