interfacets 0.1.0 → 0.9.9

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +173 -1
  3. data/LICENSE +21 -0
  4. data/Rakefile +4 -6
  5. data/lib/interfacets/client/actor.rb +20 -0
  6. data/lib/interfacets/client/assets.rb +209 -0
  7. data/lib/interfacets/client/bus.rb +69 -0
  8. data/lib/interfacets/client/channels/api.rb +95 -0
  9. data/lib/interfacets/client/channels/audio.rb +101 -0
  10. data/lib/interfacets/client/channels/base.rb +28 -0
  11. data/lib/interfacets/client/channels/page_visibility.rb +21 -0
  12. data/lib/interfacets/client/channels/react/builder.rb +203 -0
  13. data/lib/interfacets/client/channels/react/channel.rb +61 -0
  14. data/lib/interfacets/client/channels/react/dom.rb +91 -0
  15. data/lib/interfacets/client/channels/react/evaluator.rb +33 -0
  16. data/lib/interfacets/client/channels/speech_to_text.rb +100 -0
  17. data/lib/interfacets/client/channels/timer.rb +51 -0
  18. data/lib/interfacets/client/channels/url.rb +52 -0
  19. data/lib/interfacets/client/config.rb +22 -0
  20. data/lib/interfacets/client/delegator.rb +37 -0
  21. data/lib/interfacets/client/facet.rb +26 -0
  22. data/lib/interfacets/client/facet2.rb +15 -0
  23. data/lib/interfacets/client/facets/attributes/accessor.rb +28 -0
  24. data/lib/interfacets/client/facets/attributes/association.rb +50 -0
  25. data/lib/interfacets/client/facets/attributes/bind.rb +25 -0
  26. data/lib/interfacets/client/facets/attributes/collection.rb +47 -0
  27. data/lib/interfacets/client/facets/attributes/readonly.rb +19 -0
  28. data/lib/interfacets/client/facets/deserializer.rb +30 -0
  29. data/lib/interfacets/client/facets/schema/deserializer.rb +63 -0
  30. data/lib/interfacets/client/facets/schema.rb +63 -0
  31. data/lib/interfacets/client/facets/serializer.rb +18 -0
  32. data/lib/interfacets/client/registry.rb +84 -0
  33. data/lib/interfacets/client/system.rb +88 -0
  34. data/lib/interfacets/client/utils/active_support_concern.rb +220 -0
  35. data/lib/interfacets/client/utils/mruby_patches.rb +81 -0
  36. data/lib/interfacets/client/utils/open_struct.rb +102 -0
  37. data/lib/interfacets/client/utils/securerandom.rb +69 -0
  38. data/lib/interfacets/client/view.rb +47 -0
  39. data/lib/interfacets/client.rb +13 -0
  40. data/lib/interfacets/mruby/build.dockerfile +66 -0
  41. data/lib/interfacets/mruby/build_config.rb +20 -0
  42. data/lib/interfacets/mruby/entrypoint.rb +23 -0
  43. data/lib/interfacets/mruby/init.c +66 -0
  44. data/lib/interfacets/server/api.rb +64 -0
  45. data/lib/interfacets/server/assets/facet.rb +61 -0
  46. data/lib/interfacets/server/assets.rb +210 -0
  47. data/lib/interfacets/server/basic_routable.rb +40 -0
  48. data/lib/interfacets/server/basic_router.rb +74 -0
  49. data/lib/interfacets/server/bus.rb +39 -0
  50. data/lib/interfacets/server/config.rb +87 -0
  51. data/lib/interfacets/server/facet.rb +51 -0
  52. data/lib/interfacets/server/facets/deserializer.rb +25 -0
  53. data/lib/interfacets/server/facets/schema/serializer.rb +54 -0
  54. data/lib/interfacets/server/facets/serializer.rb +50 -0
  55. data/lib/interfacets/server/registry.rb +212 -0
  56. data/lib/interfacets/shared/entities/bus.rb +218 -0
  57. data/lib/interfacets/shared/entities/collection_proxy.rb +190 -0
  58. data/lib/interfacets/shared/entities/specs/handlers.rb +117 -0
  59. data/lib/interfacets/shared/entities/specs.rb +124 -0
  60. data/lib/interfacets/shared/entity.rb +178 -0
  61. data/lib/interfacets/shared/entity_collection.rb +88 -0
  62. data/lib/interfacets/shared/generated_store.rb +145 -0
  63. data/lib/interfacets/shared/utils.rb +54 -0
  64. data/lib/interfacets/shared/validations.rb +71 -0
  65. data/lib/interfacets/test/browser.rb +63 -0
  66. data/lib/interfacets/test/js/inline_bus.rb +91 -0
  67. data/lib/interfacets/test/js/nodo_bus.rb +81 -0
  68. data/lib/interfacets/test/js/receivers/api.rb +37 -0
  69. data/lib/interfacets/test/js/receivers/react/node.rb +167 -0
  70. data/lib/interfacets/test/js/receivers/react.rb +31 -0
  71. data/lib/interfacets/test/js/receivers/url.rb +55 -0
  72. data/lib/interfacets/test.rb +17 -0
  73. data/lib/interfacets/version.rb +1 -1
  74. data/lib/interfacets.rb +26 -2
  75. metadata +103 -6
  76. data/README.md +0 -35
@@ -0,0 +1,71 @@
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] << v
17
+ end
18
+
19
+ def [](k)
20
+ @errors[k]
21
+ end
22
+
23
+ def each(...)
24
+ @errors.each(...)
25
+ end
26
+
27
+ def empty?
28
+ @errors.all? { _2.empty? }
29
+ end
30
+ end
31
+
32
+ included do
33
+ def self.validators
34
+ @validators ||= []
35
+ end
36
+
37
+ def self.validate(&block)
38
+ raise(ArgumentError.new("block required")) unless block_given?
39
+
40
+ validators << block
41
+ end
42
+
43
+ def errors
44
+ @errors ||= Errors.new
45
+ end
46
+
47
+ def errors_if_changed(attr)
48
+ errors[attr].any? ? errors[attr] : nil
49
+ end
50
+
51
+ def valid?
52
+ errors.empty?
53
+ end
54
+
55
+ def validate
56
+ @errors = Errors.new
57
+ self.class.validators.each do |block|
58
+ instance_exec(&block)
59
+ end
60
+ end
61
+
62
+ # on_change do |attr, _old_val, _new_val|
63
+ # reset_validations
64
+ # end
65
+ end
66
+
67
+ class_methods do
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,63 @@
1
+
2
+ module Interfacets
3
+ module Test
4
+ class Browser
5
+ TYPES = {
6
+ inline: Js::InlineBus,
7
+ nodo: Js::NodoBus,
8
+ }
9
+
10
+ attr_reader :system_json, :router, :type
11
+ def initialize(system_json:, router:, type:)
12
+ unless TYPES.keys.include?(type)
13
+ raise ArgumentError.new(
14
+ "type: #{type.inspect} not allowed. Must be one of #{TYPES.keys}"
15
+ )
16
+ end
17
+
18
+ @type = type
19
+ # Serialize and deserialize to ensure proper data structure for JS
20
+ @system_json = JSON.parse(system_json.to_json)
21
+ @router = router
22
+ end
23
+
24
+ def visit(path)
25
+ js.init(
26
+ hydrated_facet: router.call(path).render
27
+ )
28
+ end
29
+
30
+ def c(channel_id)
31
+ js.handler(channel_id)
32
+ end
33
+
34
+ def dispatch(channel_id, event)
35
+ js.dispatch(channel_id, event)
36
+ end
37
+
38
+ def url
39
+ c("url")
40
+ end
41
+
42
+ def dom
43
+ c("dom")
44
+ end
45
+
46
+ private
47
+
48
+ def js
49
+ @js ||= (
50
+ js_api = Test::Js::Receivers::Api.new(name: "interfacets:api", router:)
51
+ js_react = Test::Js::Receivers::React.new(name: "dom")
52
+ js_url = Test::Js::Receivers::Url.new(name: "url")
53
+ receivers = [js_api, js_react, js_url]
54
+
55
+ TYPES.fetch(type).new(
56
+ receiver_index: receivers.map { [_1.name, _1] }.to_h,
57
+ client_system_json: system_json,
58
+ )
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,91 @@
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"],
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
+ private
34
+
35
+ def handle(event)
36
+ case event.fetch("type")
37
+ when "interfacets:system:render"
38
+ bus_event = event.fetch("payload")
39
+ bus_id = bus_event.fetch("id")
40
+
41
+ bus_event.fetch("payload").each do |channel_event|
42
+ channel_id = channel_event.fetch("id")
43
+ receiver_index
44
+ .fetch(channel_id)
45
+ .receive(
46
+ payload: channel_event.fetch("payload"),
47
+ dispatch: ->(ev) {
48
+ dispatch(bus_id:, channel_id:, event: ev)
49
+ },
50
+ )
51
+ end
52
+ else
53
+ raise("unhandled event type: #{event}")
54
+ end
55
+ end
56
+
57
+ def dispatch(bus_id:, channel_id:, event:)
58
+ client.handle(
59
+ H.j(
60
+ {
61
+ destination: { bus: bus_id, channel: channel_id },
62
+ type: "interfacets:channel:event",
63
+ payload: event,
64
+ },
65
+ ),
66
+ )
67
+ end
68
+
69
+ def client
70
+ @client ||= (
71
+ $asset_logger = Logger.new("/dev/null")
72
+ original_verbose = $VERBOSE
73
+ $VERBOSE = nil
74
+ begin
75
+ Client::Assets.bootstrap(client_system_json.fetch("assets"))
76
+ ensure
77
+ $VERBOSE = original_verbose
78
+ end
79
+ Client::System.logger = $asset_logger
80
+
81
+ Client.start(
82
+ transmit: ->(event) {
83
+ handle(H.j(event))
84
+ },
85
+ )
86
+ )
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,81 @@
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
+ ch.receive(
34
+ payload: js_get_state(id).fetch("data"),
35
+ dispatch: ->(e) { dispatch(id, e) }
36
+ )
37
+ end
38
+ end
39
+
40
+ import app: File.join(__dir__, "../../../../test/wasm/app.mjs")
41
+ import :fs
42
+
43
+ function :js_init, <<~JS
44
+ async (config) => {
45
+ global.logs = []
46
+ global.console = {
47
+ log: (...msgs) => {
48
+ global.logs.push({
49
+ type: "log",
50
+ value: msgs.map(m => m.toString())
51
+ })
52
+ },
53
+ error: (...msgs) => {
54
+ global.logs.push({
55
+ type: "error",
56
+ value: msgs.map(m => m.toString())
57
+ })
58
+ },
59
+ }
60
+
61
+ await app.init(config)
62
+ }
63
+ JS
64
+
65
+ function :js_dispatch, <<~JS
66
+ (channelName, event) => {
67
+ app.dispatch(channelName, event)
68
+ }
69
+ JS
70
+
71
+ function :js_get_logs, <<~JS
72
+ () => global.logs
73
+ JS
74
+
75
+ function :js_get_state, <<~JS
76
+ (channelName) => app.getState(channelName)
77
+ JS
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,37 @@
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
+ url = payload.dig("streams", "default", "url")
29
+ payload
30
+ .dig("streams", "default", "body", "event", "payload")
31
+ .then { response_queue << router.call(url).handle(_1) }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,167 @@
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
13
+ def initialize(json)
14
+ @json = json
15
+ end
16
+
17
+ def call
18
+ xml = Nokogiri::XML.fragment("<Facet/>")
19
+ json
20
+ .dig("streams", "default", "dom")
21
+ .map { parse_element(_1) }
22
+ .each { xml.add_child(_1) }
23
+ xml
24
+ end
25
+
26
+ private
27
+
28
+ def parse_attribute(value)
29
+ case value
30
+ when Array
31
+ when Hash
32
+ value.transform_values { parse_attribute(_1) }
33
+ when Numeric, String, TrueClass, FalseClass, NilClass
34
+ value
35
+ else
36
+ raise "unknown attribute type: #{value}"
37
+ end
38
+ end
39
+
40
+ def parse_element(el)
41
+ case el.fetch("type")
42
+ when "interfacets:string-node"
43
+ el.dig("attributes", "value")
44
+ when "interfacets:react-dom:element"
45
+ xml = (
46
+ el
47
+ .fetch("element")
48
+ .then { Nokogiri::XML.fragment("<#{_1} />").children.first }
49
+ )
50
+
51
+ el.fetch("children").each do |child|
52
+ xml.add_child(parse_element(child)) if child
53
+ end
54
+
55
+ el.fetch("attributes").each do |name, value|
56
+ xml.set_attribute(name, parse_attribute(value).to_json)
57
+ end
58
+
59
+ xml
60
+ else
61
+ raise "unknown type: #{el.fetch("type")}"
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.parse(json:, dispatch:)
67
+ new(
68
+ xml: XmlParser.new(json).call,
69
+ dispatch:,
70
+ parent: nil,
71
+ )
72
+ end
73
+
74
+ Error = Class.new(StandardError)
75
+ NoMatchesErrorError = Class.new(Error)
76
+ MultipleMatchesError = Class.new(Error)
77
+ StaleNodeError = Class.new(Error) do
78
+ def initialize
79
+ super(
80
+ <<~TXT
81
+ This node is stale and cannot be used. This occurs when the facet \
82
+ has been updated and you are using a reference to an out-of-date \
83
+ node. Re-fetch the node using `page.dom` rather than storing a \
84
+ reference to the node.
85
+ TXT
86
+ )
87
+ end
88
+ end
89
+
90
+ attr_reader :xml, :dispatch, :parent
91
+ def initialize(xml:, dispatch:, parent:)
92
+ @xml = xml
93
+ @dispatch = dispatch
94
+ @parent = parent
95
+ end
96
+
97
+ def stale!
98
+ @stale = true
99
+ end
100
+
101
+ def stale?
102
+ @stale || parent&.stale?
103
+ end
104
+
105
+ def content
106
+ raise StaleNodeError if stale?
107
+
108
+ xml.content
109
+ end
110
+
111
+ def one(...)
112
+ raise StaleNodeError if stale?
113
+
114
+ all(...)
115
+ .tap {
116
+ if _1.count == 0
117
+ raise NoMatchesError
118
+ elsif _1.count > 1
119
+ raise MultipleMatchesError.new(_1.map(&:content).join(", "))
120
+ end
121
+ }
122
+ .first
123
+ end
124
+
125
+ EMPTY_ARG = Object.new
126
+
127
+ def all(*a, content: EMPTY_ARG, **p)
128
+ raise StaleNodeError if stale?
129
+
130
+ xml
131
+ .css(*a, **p)
132
+ .map { Node.new(xml: _1, dispatch:, parent: self) }
133
+ .select {
134
+ (
135
+ content == EMPTY_ARG || (
136
+ if content.is_a?(Regexp)
137
+ _1.content.match(content)
138
+ else
139
+ _1.content == content
140
+ end
141
+ )
142
+ )
143
+ }
144
+
145
+ end
146
+
147
+ def attribute(name)
148
+ raise StaleNodeError if stale?
149
+
150
+ xml
151
+ .attribute(name)
152
+ .then { JSON.parse(_1) }
153
+ end
154
+
155
+ def trigger(name, data = {})
156
+ raise StaleNodeError if stale?
157
+
158
+ value = attribute(name.to_s)
159
+ value["payload"]["event"] = data
160
+ dispatch.(value)
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,31 @@
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
12
+ def initialize(name:)
13
+ @name = name
14
+ @actions = {}
15
+ end
16
+
17
+ def receive(payload:, dispatch:)
18
+ @dispatch = dispatch
19
+ @actions = nil
20
+ @node&.stale!
21
+ @node = Node.parse(json: payload, dispatch:)
22
+ end
23
+
24
+ def handler
25
+ @node
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Interfacets
6
+ module Test
7
+ module Js
8
+ module Receivers
9
+ class Url
10
+ class Handler
11
+ def initialize(state)
12
+ @state = state
13
+ end
14
+
15
+ def url
16
+ url_spec = @state.fetch("urlSpec")
17
+ base_url = url_spec["url"]
18
+ query_params = url_spec["queryParams"]
19
+
20
+ return base_url if query_params.nil? || query_params.empty?
21
+
22
+ uri = URI(base_url)
23
+ uri.query = URI.encode_www_form(query_params)
24
+ uri.to_s
25
+ end
26
+
27
+ def redirected_url
28
+ @state.fetch("redirectSpec")
29
+ end
30
+ end
31
+
32
+ attr_reader :server, :name, :response_queue
33
+ def initialize(name:)
34
+ @name = name
35
+ end
36
+
37
+ def receive(payload:, dispatch:)
38
+ @dispatch = dispatch
39
+
40
+ return if payload.nil?
41
+ return if payload.empty?
42
+ return if payload.dig("streams", "default").nil?
43
+ return if payload.dig("streams", "default").empty?
44
+
45
+ @handler = Handler.new(payload.dig("streams", "default"))
46
+ end
47
+
48
+ def handler
49
+ @handler
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require_relative "./test/session"
4
+ # require_relative "./test/js"
5
+ # require_relative "./test/js/channels"
6
+
7
+ module Interfacets
8
+ module Test
9
+ module H
10
+ module_function
11
+
12
+ def j(hash)
13
+ JSON.parse(hash.to_json)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Interfacets
4
- VERSION = "0.1.0"
4
+ VERSION = "0.9.9"
5
5
  end
data/lib/interfacets.rb CHANGED
@@ -1,8 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "interfacets/version"
3
+ require "zeitwerk"
4
4
 
5
5
  module Interfacets
6
+ class << self
7
+ def reload
8
+ loader.reload
9
+ end
10
+
11
+ def loader
12
+ @loader ||= (
13
+ Zeitwerk::Loader
14
+ .for_gem
15
+ .tap { _1.ignore("#{__dir__}/interfacets/mruby") }
16
+ .tap { _1.ignore("#{__dir__}/interfacets/client/utils") }
17
+ .tap { _1.enable_reloading if enable_reloading? }
18
+ .tap(&:setup)
19
+ )
20
+ end
21
+
22
+ def enable_reloading?
23
+ $interfacets_dev_mode
24
+ end
25
+ end
26
+
6
27
  class Error < StandardError; end
7
- # Your code goes here...
28
+ end
29
+
30
+ unless Interfacets.enable_reloading?
31
+ Interfacets.loader
8
32
  end