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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +173 -1
  3. data/.tmp +5 -0
  4. data/LICENSE +21 -0
  5. data/Rakefile +9 -7
  6. data/lib/interfacets/client/actor.rb +20 -0
  7. data/lib/interfacets/client/assets.rb +210 -0
  8. data/lib/interfacets/client/bus.rb +73 -0
  9. data/lib/interfacets/client/channels/api.rb +102 -0
  10. data/lib/interfacets/client/channels/audio.rb +101 -0
  11. data/lib/interfacets/client/channels/base.rb +28 -0
  12. data/lib/interfacets/client/channels/page_visibility.rb +21 -0
  13. data/lib/interfacets/client/channels/react/builder.rb +203 -0
  14. data/lib/interfacets/client/channels/react/channel.rb +61 -0
  15. data/lib/interfacets/client/channels/react/dom.rb +91 -0
  16. data/lib/interfacets/client/channels/react/evaluator.rb +33 -0
  17. data/lib/interfacets/client/channels/speech_to_text.rb +100 -0
  18. data/lib/interfacets/client/channels/timer.rb +51 -0
  19. data/lib/interfacets/client/channels/url.rb +52 -0
  20. data/lib/interfacets/client/config.rb +22 -0
  21. data/lib/interfacets/client/delegator.rb +37 -0
  22. data/lib/interfacets/client/registry.rb +23 -0
  23. data/lib/interfacets/client/system.rb +88 -0
  24. data/lib/interfacets/client/utils/active_support_concern.rb +220 -0
  25. data/lib/interfacets/client/utils/mruby_patches.rb +81 -0
  26. data/lib/interfacets/client/utils/open_struct.rb +115 -0
  27. data/lib/interfacets/client/utils/securerandom.rb +69 -0
  28. data/lib/interfacets/client.rb +13 -0
  29. data/lib/interfacets/component_registry.rb +115 -0
  30. data/lib/interfacets/component_schema_parser.rb +84 -0
  31. data/lib/interfacets/mruby/build.dockerfile +66 -0
  32. data/lib/interfacets/mruby/build_config.rb +20 -0
  33. data/lib/interfacets/mruby/entrypoint.rb +23 -0
  34. data/lib/interfacets/mruby/init.c +66 -0
  35. data/lib/interfacets/server/api.rb +44 -0
  36. data/lib/interfacets/server/assets/facet.rb +63 -0
  37. data/lib/interfacets/server/assets.rb +216 -0
  38. data/lib/interfacets/server/basic_router.rb +79 -0
  39. data/lib/interfacets/server/bus.rb +34 -0
  40. data/lib/interfacets/server/config.rb +87 -0
  41. data/lib/interfacets/server/facets/deserializer.rb +25 -0
  42. data/lib/interfacets/server/facets/schema/serializer.rb +54 -0
  43. data/lib/interfacets/server/facets/serializer.rb +50 -0
  44. data/lib/interfacets/server/registry.rb +51 -0
  45. data/lib/interfacets/shared/basic_routable.rb +45 -0
  46. data/lib/interfacets/shared/entities/bus.rb +230 -0
  47. data/lib/interfacets/shared/entities/collection_proxy.rb +190 -0
  48. data/lib/interfacets/shared/entities/specs/handlers.rb +133 -0
  49. data/lib/interfacets/shared/entities/specs.rb +161 -0
  50. data/lib/interfacets/shared/entity.rb +102 -0
  51. data/lib/interfacets/shared/entity_dsl.rb +154 -0
  52. data/lib/interfacets/shared/facet.rb +200 -0
  53. data/lib/interfacets/shared/generated_store.rb +149 -0
  54. data/lib/interfacets/shared/utils.rb +54 -0
  55. data/lib/interfacets/shared/validations.rb +75 -0
  56. data/lib/interfacets/shared/view.rb +74 -0
  57. data/lib/interfacets/test/component_registry.rb +63 -0
  58. data/lib/interfacets/test/js/inline_bus.rb +100 -0
  59. data/lib/interfacets/test/js/nodo_bus.rb +98 -0
  60. data/lib/interfacets/test/js/receivers/api.rb +48 -0
  61. data/lib/interfacets/test/js/receivers/react/node/xml_parser.rb +75 -0
  62. data/lib/interfacets/test/js/receivers/react/node.rb +133 -0
  63. data/lib/interfacets/test/js/receivers/react.rb +32 -0
  64. data/lib/interfacets/test/js/receivers/timer.rb +77 -0
  65. data/lib/interfacets/test/js/receivers/url.rb +60 -0
  66. data/lib/interfacets/test/standard_elements.yml +173 -0
  67. data/lib/interfacets/test/ui_simulator.rb +75 -0
  68. data/lib/interfacets/test/validation_engine.rb +151 -0
  69. data/lib/interfacets/test.rb +13 -0
  70. data/lib/interfacets/version.rb +1 -1
  71. data/lib/interfacets.rb +29 -2
  72. metadata +114 -6
  73. 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