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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interfacets
|
|
4
|
+
module Shared
|
|
5
|
+
module Validations
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class Errors
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@errors = Hash.new { |h, k| h[k] = [] }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add(k, v)
|
|
16
|
+
@errors[k.to_sym] << v
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def clear
|
|
20
|
+
@errors.clear
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def [](k)
|
|
24
|
+
@errors[k]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def each(...)
|
|
28
|
+
@errors.each(...)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def empty?
|
|
32
|
+
@errors.all? { _2.empty? }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
included do
|
|
37
|
+
def self.validators
|
|
38
|
+
@validators ||= []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.validate(&block)
|
|
42
|
+
raise(ArgumentError.new("block required")) unless block_given?
|
|
43
|
+
|
|
44
|
+
validators << block
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def errors
|
|
48
|
+
@errors ||= Errors.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def errors_if_changed(attr)
|
|
52
|
+
errors[attr].any? ? errors[attr] : nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def valid?
|
|
56
|
+
errors.empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate
|
|
60
|
+
@errors = Errors.new
|
|
61
|
+
self.class.validators.each do |block|
|
|
62
|
+
instance_exec(&block)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# on_change do |attr, _old_val, _new_val|
|
|
67
|
+
# reset_validations
|
|
68
|
+
# end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class_methods do
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interfacets
|
|
4
|
+
module Shared
|
|
5
|
+
class View
|
|
6
|
+
class Evaluator
|
|
7
|
+
def self.call(**p)
|
|
8
|
+
new(**p).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :entity, :channels, :block
|
|
12
|
+
def initialize(entity:, channels:, block:)
|
|
13
|
+
@entity = entity
|
|
14
|
+
@channels = channels
|
|
15
|
+
@block = block
|
|
16
|
+
@data = Hash.new { |h, k| h[k] = { streams: {} } }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
instance_exec(entity, &@block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render(entity)
|
|
24
|
+
entity
|
|
25
|
+
.class
|
|
26
|
+
.view
|
|
27
|
+
.render(
|
|
28
|
+
entity:,
|
|
29
|
+
channels:
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render_to(channel_id, stream: "default", &block)
|
|
34
|
+
channel_id = channel_id.to_s
|
|
35
|
+
|
|
36
|
+
channels
|
|
37
|
+
.fetch(channel_id)
|
|
38
|
+
.builder(stream.to_s)
|
|
39
|
+
.then { @current_builder = _1 }
|
|
40
|
+
.then { instance_exec(_1, &block) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def channel(name)
|
|
44
|
+
@channels.fetch(name)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
def view(&block)
|
|
50
|
+
@view = block if block_given?
|
|
51
|
+
@view
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def view=(val)
|
|
55
|
+
@view = val
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render(**p)
|
|
59
|
+
new.render(**p)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render(entity:, channels:)
|
|
64
|
+
return unless self.class.view
|
|
65
|
+
|
|
66
|
+
Evaluator.call(
|
|
67
|
+
entity: entity,
|
|
68
|
+
channels: channels,
|
|
69
|
+
block: self.class.view
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Interfacets
|
|
6
|
+
module Test
|
|
7
|
+
class ComponentRegistry
|
|
8
|
+
DEFAULT_CONFIG_PATH = "config/interfacets/components.yml"
|
|
9
|
+
|
|
10
|
+
def self.load(path = DEFAULT_CONFIG_PATH)
|
|
11
|
+
new(path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :contracts
|
|
15
|
+
|
|
16
|
+
def initialize(path)
|
|
17
|
+
@path = path
|
|
18
|
+
@schema_cache = {}
|
|
19
|
+
@contracts = load_contracts
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_component(name)
|
|
23
|
+
@contracts[name.to_s]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def definitions
|
|
27
|
+
@contracts["definitions"] || {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def load_contracts
|
|
33
|
+
return {} unless File.exist?(@path)
|
|
34
|
+
|
|
35
|
+
contracts = YAML.load_file(@path, aliases: true) || {}
|
|
36
|
+
contracts.each do |name, config|
|
|
37
|
+
next unless config.is_a?(Hash)
|
|
38
|
+
|
|
39
|
+
schema = config["schema"]
|
|
40
|
+
if schema.is_a?(String)
|
|
41
|
+
config["schema"] = load_schema_file(name, schema)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
contracts
|
|
45
|
+
rescue Psych::SyntaxError => e
|
|
46
|
+
raise Error, "Failed to parse component registry at #{@path}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def load_schema_file(component_name, schema_path)
|
|
50
|
+
full_path = File.expand_path(schema_path, File.dirname(@path))
|
|
51
|
+
return @schema_cache[full_path] if @schema_cache.key?(full_path)
|
|
52
|
+
|
|
53
|
+
raise Errno::ENOENT, "Schema file not found for component '#{component_name}' at #{full_path}" unless File.exist?(full_path)
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
@schema_cache[full_path] = YAML.load_file(full_path, aliases: true) || {}
|
|
57
|
+
rescue Psych::SyntaxError => e
|
|
58
|
+
raise Error, "Failed to parse schema file for component '#{component_name}' at #{full_path}: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require "nodo"
|
|
2
|
+
|
|
3
|
+
module Interfacets
|
|
4
|
+
module Test
|
|
5
|
+
module Js
|
|
6
|
+
class InlineBus
|
|
7
|
+
attr_reader :receiver_index, :client_system_json
|
|
8
|
+
def initialize(receiver_index:, client_system_json:)
|
|
9
|
+
@receiver_index = receiver_index
|
|
10
|
+
@client_system_json = client_system_json
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def handler(channel_id)
|
|
14
|
+
receiver_index.fetch(channel_id).handler
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def init(hydrated_facet:)
|
|
18
|
+
client.handle(H.j({
|
|
19
|
+
type: "interfacets:system:create_bus",
|
|
20
|
+
payload: {
|
|
21
|
+
id: "default",
|
|
22
|
+
channel_ids: ["interfacets:api", "dom", "url", "timer"],
|
|
23
|
+
hydration: {
|
|
24
|
+
destination: { bus: "default", channel: "interfacets:api" },
|
|
25
|
+
type: "interfacets:api:hydrate",
|
|
26
|
+
payload: hydrated_facet
|
|
27
|
+
},
|
|
28
|
+
config: client_system_json,
|
|
29
|
+
}
|
|
30
|
+
}))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dispatch(channel_id, event)
|
|
34
|
+
dispatch_to_bus(bus_id: "default", channel_id: channel_id, event: event)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def handle(event)
|
|
40
|
+
case event.fetch("type")
|
|
41
|
+
when "interfacets:system:render"
|
|
42
|
+
bus_event = event.fetch("payload")
|
|
43
|
+
bus_id = bus_event.fetch("id")
|
|
44
|
+
|
|
45
|
+
bus_event.fetch("payload").each do |channel_event|
|
|
46
|
+
channel_id = channel_event.fetch("id")
|
|
47
|
+
receiver = receiver_index.fetch(channel_id)
|
|
48
|
+
|
|
49
|
+
receiver
|
|
50
|
+
.receive(
|
|
51
|
+
payload: channel_event.fetch("payload"),
|
|
52
|
+
dispatch: ->(ev) {
|
|
53
|
+
dispatch_to_bus(bus_id:, channel_id:, event: ev)
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
raise("unhandled event type: #{event}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Flush any responses that may have come from the server
|
|
62
|
+
# should separate this out so that auto-flush or manual-flush
|
|
63
|
+
# is possible to simulate ordering events as needed.
|
|
64
|
+
receiver_index.each do |channel_id, receiver|
|
|
65
|
+
if receiver.respond_to?(:response_queue)
|
|
66
|
+
receiver.flush_responses.each do |response|
|
|
67
|
+
dispatch_to_bus(bus_id:, channel_id:, event: response)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dispatch_to_bus(bus_id:, channel_id:, event:)
|
|
74
|
+
client.handle(
|
|
75
|
+
H.j(
|
|
76
|
+
{
|
|
77
|
+
destination: { bus: bus_id, channel: channel_id },
|
|
78
|
+
type: "interfacets:channel:event",
|
|
79
|
+
payload: event,
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def client
|
|
86
|
+
@client ||= (
|
|
87
|
+
$asset_logger = Logger.new("/dev/null")
|
|
88
|
+
Client::System.logger = $asset_logger
|
|
89
|
+
|
|
90
|
+
Client.start(
|
|
91
|
+
transmit: ->(event) {
|
|
92
|
+
handle(H.j(event))
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require "nodo"
|
|
2
|
+
|
|
3
|
+
module Interfacets
|
|
4
|
+
module Test
|
|
5
|
+
module Js
|
|
6
|
+
class NodoBus < Nodo::Core
|
|
7
|
+
attr_reader :receiver_index, :client_system_json
|
|
8
|
+
def initialize(receiver_index:, client_system_json:)
|
|
9
|
+
super()
|
|
10
|
+
@receiver_index = receiver_index
|
|
11
|
+
@client_system_json = client_system_json
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def dispatch(channel_id, event)
|
|
15
|
+
js_dispatch(channel_id, event)
|
|
16
|
+
render
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def handler(channel_id)
|
|
20
|
+
receiver_index.fetch(channel_id).handler
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def init(hydrated_facet:)
|
|
24
|
+
js_init(
|
|
25
|
+
clientSystemJson: client_system_json,
|
|
26
|
+
hydratedFacet: hydrated_facet
|
|
27
|
+
)
|
|
28
|
+
render
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render
|
|
32
|
+
receiver_index.each do |id, ch|
|
|
33
|
+
state = js_get_state(id)
|
|
34
|
+
next if state.nil?
|
|
35
|
+
|
|
36
|
+
ch.receive(
|
|
37
|
+
payload: state.fetch("data"),
|
|
38
|
+
dispatch: ->(e) { dispatch(id, e) }
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
receiver_index.each do |id, ch|
|
|
42
|
+
if ch.respond_to?(:response_queue)
|
|
43
|
+
ch.flush_responses.each do |response|
|
|
44
|
+
dispatch(id, response)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if js_get_logs.any? { _1.fetch("type") == "error" }
|
|
51
|
+
js_get_logs
|
|
52
|
+
.map { _1.fetch("value") }
|
|
53
|
+
.then { raise(_1.join("\n")) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
import app: File.join(__dir__, "../../../../test/wasm/app.mjs")
|
|
58
|
+
import :fs
|
|
59
|
+
|
|
60
|
+
function :js_init, <<~JS
|
|
61
|
+
async (config) => {
|
|
62
|
+
global.logs = []
|
|
63
|
+
global.console = {
|
|
64
|
+
log: (...msgs) => {
|
|
65
|
+
global.logs.push({
|
|
66
|
+
type: "log",
|
|
67
|
+
value: msgs.map(m => m.toString())
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
error: (...msgs) => {
|
|
71
|
+
global.logs.push({
|
|
72
|
+
type: "error",
|
|
73
|
+
value: msgs.map(m => m.toString())
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await app.init(config)
|
|
79
|
+
}
|
|
80
|
+
JS
|
|
81
|
+
|
|
82
|
+
function :js_dispatch, <<~JS
|
|
83
|
+
(channelName, event) => {
|
|
84
|
+
app.dispatch(channelName, event)
|
|
85
|
+
}
|
|
86
|
+
JS
|
|
87
|
+
|
|
88
|
+
function :js_get_logs, <<~JS
|
|
89
|
+
() => global.logs
|
|
90
|
+
JS
|
|
91
|
+
|
|
92
|
+
function :js_get_state, <<~JS
|
|
93
|
+
(channelName) => app.getState(channelName)
|
|
94
|
+
JS
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# require_relative "./channels/react"
|
|
4
|
+
|
|
5
|
+
module Interfacets
|
|
6
|
+
module Test
|
|
7
|
+
module Js
|
|
8
|
+
module Receivers
|
|
9
|
+
class Api
|
|
10
|
+
attr_reader :router, :name, :response_queue
|
|
11
|
+
def initialize(name:, router:)
|
|
12
|
+
@name = name
|
|
13
|
+
@router = router
|
|
14
|
+
@response_queue = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def handler
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def receive(payload:, dispatch:)
|
|
21
|
+
@dispatch = dispatch
|
|
22
|
+
|
|
23
|
+
return if payload.nil?
|
|
24
|
+
return if payload.empty?
|
|
25
|
+
return if payload.dig("streams", "default").nil?
|
|
26
|
+
return if payload.dig("streams", "default").empty?
|
|
27
|
+
|
|
28
|
+
method = payload.dig("streams", "default", "method")
|
|
29
|
+
url = payload.dig("streams", "default", "url")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if method == "get"
|
|
33
|
+
response_queue << router.call(url).render
|
|
34
|
+
else
|
|
35
|
+
payload
|
|
36
|
+
.dig("streams", "default", "body", "event", "payload")
|
|
37
|
+
.then { response_queue << router.call(url).handle(_1) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def flush_responses
|
|
42
|
+
response_queue.tap { @response_queue = [] }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Interfacets
|
|
6
|
+
module Test
|
|
7
|
+
module Js
|
|
8
|
+
module Receivers
|
|
9
|
+
class React
|
|
10
|
+
class Node
|
|
11
|
+
class XmlParser
|
|
12
|
+
attr_reader :json, :validation_engine
|
|
13
|
+
def initialize(json, validation_engine:)
|
|
14
|
+
@json = json
|
|
15
|
+
@validation_engine = validation_engine
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
xml = Nokogiri::XML.fragment("<Facet/>")
|
|
20
|
+
json
|
|
21
|
+
.dig("streams", "default", "dom")
|
|
22
|
+
.map { parse_element(_1) }
|
|
23
|
+
.each { xml.add_child(_1) }
|
|
24
|
+
xml
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def parse_attribute(value, top: true)
|
|
30
|
+
case value
|
|
31
|
+
when Array
|
|
32
|
+
when Hash
|
|
33
|
+
value
|
|
34
|
+
.transform_values { parse_attribute(_1, top: false) }
|
|
35
|
+
# we don't want to call to_json on nested hashes
|
|
36
|
+
.then { top ? _1.to_json : _1 }
|
|
37
|
+
when Numeric, String, TrueClass, FalseClass, NilClass
|
|
38
|
+
value
|
|
39
|
+
else
|
|
40
|
+
raise "unknown attribute type: #{value}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_element(el)
|
|
45
|
+
case el.fetch("type")
|
|
46
|
+
when "interfacets:string-node"
|
|
47
|
+
el.dig("attributes", "value").to_s
|
|
48
|
+
when "interfacets:react-dom:element"
|
|
49
|
+
xml = (
|
|
50
|
+
el
|
|
51
|
+
.fetch("element")
|
|
52
|
+
.tap { validation_engine&.validate_props(_1, el.fetch("attributes")) }
|
|
53
|
+
.then { Nokogiri::XML.fragment("<#{_1} />").children.first }
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
el.fetch("children").each do |child|
|
|
57
|
+
xml.add_child(parse_element(child)) if child
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
el.fetch("attributes").each do |name, value|
|
|
61
|
+
xml.set_attribute(name, parse_attribute(value))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
xml
|
|
65
|
+
else
|
|
66
|
+
raise "unknown type: #{el.fetch("type")}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Interfacets
|
|
6
|
+
module Test
|
|
7
|
+
module Js
|
|
8
|
+
module Receivers
|
|
9
|
+
class React
|
|
10
|
+
class Node
|
|
11
|
+
def self.parse(json:, dispatch:, validation_engine:)
|
|
12
|
+
new(
|
|
13
|
+
xml: XmlParser.new(json, validation_engine:).call,
|
|
14
|
+
dispatch:,
|
|
15
|
+
validation_engine:,
|
|
16
|
+
parent: nil,
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Error = Class.new(StandardError)
|
|
21
|
+
NoMatchesError = Class.new(Error)
|
|
22
|
+
MultipleMatchesError = Class.new(Error)
|
|
23
|
+
StaleNodeError = Class.new(Error) do
|
|
24
|
+
def initialize
|
|
25
|
+
super(
|
|
26
|
+
<<~TXT
|
|
27
|
+
This node is stale and cannot be used. This occurs when the facet \
|
|
28
|
+
has been updated and you are using a reference to an out-of-date \
|
|
29
|
+
node. Re-fetch the node using `page.dom` rather than storing a \
|
|
30
|
+
reference to the node.
|
|
31
|
+
TXT
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :xml, :dispatch, :parent, :validation_engine
|
|
37
|
+
def initialize(xml:, dispatch:, parent:, validation_engine:)
|
|
38
|
+
@xml = xml
|
|
39
|
+
@dispatch = dispatch
|
|
40
|
+
@parent = parent
|
|
41
|
+
@validation_engine = validation_engine
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stale!
|
|
45
|
+
@stale = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def stale?
|
|
49
|
+
@stale || parent&.stale?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def component_name
|
|
53
|
+
raise StaleNodeError if stale?
|
|
54
|
+
|
|
55
|
+
xml.name
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def props
|
|
59
|
+
raise StaleNodeError if stale?
|
|
60
|
+
|
|
61
|
+
xml.attributes.transform_values do |attr|
|
|
62
|
+
attr.value.then { JSON.parse(_1) rescue _1 }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def content
|
|
67
|
+
raise StaleNodeError if stale?
|
|
68
|
+
|
|
69
|
+
xml.content
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def one(...)
|
|
73
|
+
raise StaleNodeError if stale?
|
|
74
|
+
|
|
75
|
+
all(...)
|
|
76
|
+
.tap {
|
|
77
|
+
if _1.count == 0
|
|
78
|
+
raise NoMatchesError
|
|
79
|
+
elsif _1.count > 1
|
|
80
|
+
raise MultipleMatchesError.new(_1.map(&:content).join(", "))
|
|
81
|
+
end
|
|
82
|
+
}
|
|
83
|
+
.first
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
EMPTY_ARG = Object.new
|
|
87
|
+
|
|
88
|
+
def all(*a, content: EMPTY_ARG, **p)
|
|
89
|
+
raise StaleNodeError if stale?
|
|
90
|
+
|
|
91
|
+
xml
|
|
92
|
+
.css(*a, **p)
|
|
93
|
+
.map { Node.new(xml: _1, dispatch:, parent: self, validation_engine:) }
|
|
94
|
+
.select {
|
|
95
|
+
(
|
|
96
|
+
content == EMPTY_ARG || (
|
|
97
|
+
if content.is_a?(Regexp)
|
|
98
|
+
_1.content.match?(content)
|
|
99
|
+
else
|
|
100
|
+
_1.content == content
|
|
101
|
+
end
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def attribute(name)
|
|
109
|
+
raise StaleNodeError if stale?
|
|
110
|
+
|
|
111
|
+
xml
|
|
112
|
+
.attribute(name)
|
|
113
|
+
&.value
|
|
114
|
+
&.then { JSON.parse(_1) rescue _1 }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def trigger(name, data = {})
|
|
118
|
+
raise StaleNodeError if stale?
|
|
119
|
+
|
|
120
|
+
validation_engine&.validate_event(xml.name, name, data)
|
|
121
|
+
|
|
122
|
+
value = attribute(name.to_s)
|
|
123
|
+
raise "No event handler registered for: #{name}" unless value
|
|
124
|
+
|
|
125
|
+
value["payload"]["event"] = data
|
|
126
|
+
dispatch.(value)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require_relative "./react/node"
|
|
5
|
+
|
|
6
|
+
module Interfacets
|
|
7
|
+
module Test
|
|
8
|
+
module Js
|
|
9
|
+
module Receivers
|
|
10
|
+
class React
|
|
11
|
+
attr_reader :name, :node, :validation_engine
|
|
12
|
+
def initialize(name:, validation_engine: nil)
|
|
13
|
+
@name = name
|
|
14
|
+
@validation_engine = validation_engine
|
|
15
|
+
@actions = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def receive(payload:, dispatch:)
|
|
19
|
+
@dispatch = dispatch
|
|
20
|
+
@actions = nil
|
|
21
|
+
@node&.stale!
|
|
22
|
+
@node = Node.parse(json: payload, dispatch:, validation_engine:)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def handler
|
|
26
|
+
@node
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|