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,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Test
5
+ module Js
6
+ module Receivers
7
+ class Timer
8
+ include Enumerable
9
+
10
+ class Registration
11
+ attr_reader :ms, :response_payload, :dispatch
12
+
13
+ def initialize(ms:, response_payload:, dispatch:)
14
+ @ms = ms
15
+ @response_payload = response_payload
16
+ @dispatch = dispatch
17
+ @notified = false
18
+ end
19
+
20
+ def notify
21
+ return if @notified
22
+ @dispatch.call({ payload: response_payload })
23
+ @notified = true
24
+ end
25
+ end
26
+
27
+ attr_reader :name, :registrations
28
+ def initialize(name:)
29
+ @name = name
30
+ @registrations = []
31
+ end
32
+
33
+ def receive(payload:, dispatch:)
34
+ @dispatch = dispatch
35
+
36
+ return if payload.nil?
37
+ return if payload.empty?
38
+ return if payload.dig("streams", "default").nil?
39
+ return if payload.dig("streams", "default").empty?
40
+
41
+ event = payload.dig("streams", "default")
42
+ ms = event["ms"]
43
+ response_payload = event["response"]
44
+
45
+ return unless ms && response_payload
46
+
47
+ @registrations << Registration.new(
48
+ ms: ms,
49
+ response_payload: response_payload,
50
+ dispatch: dispatch
51
+ )
52
+ end
53
+
54
+ def handler
55
+ self
56
+ end
57
+
58
+ def first
59
+ registrations.first
60
+ end
61
+
62
+ def last
63
+ registrations.last
64
+ end
65
+
66
+ def each(&block)
67
+ registrations.each(&block)
68
+ end
69
+
70
+ def clear
71
+ @registrations = []
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,60 @@
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
+ @response_queue = []
36
+ end
37
+
38
+ def receive(payload:, dispatch:)
39
+ @dispatch = dispatch
40
+
41
+ return if payload.nil?
42
+ return if payload.empty?
43
+ return if payload.dig("streams", "default").nil?
44
+ return if payload.dig("streams", "default").empty?
45
+
46
+ @handler = Handler.new(payload.dig("streams", "default"))
47
+ end
48
+
49
+ def handler
50
+ @handler
51
+ end
52
+
53
+ def flush_responses
54
+ response_queue.tap { @response_queue = [] }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,173 @@
1
+ ---
2
+ - a
3
+ - abbr
4
+ - address
5
+ - area
6
+ - article
7
+ - aside
8
+ - audio
9
+ - b
10
+ - base
11
+ - bdi
12
+ - bdo
13
+ - blockquote
14
+ - body
15
+ - br
16
+ - button
17
+ - canvas
18
+ - caption
19
+ - cite
20
+ - code
21
+ - col
22
+ - colgroup
23
+ - data
24
+ - datalist
25
+ - dd
26
+ - del
27
+ - details
28
+ - dfn
29
+ - dialog
30
+ - div
31
+ - dl
32
+ - dt
33
+ - em
34
+ - embed
35
+ - fieldset
36
+ - figcaption
37
+ - figure
38
+ - footer
39
+ - form
40
+ - h1
41
+ - h2
42
+ - h3
43
+ - h4
44
+ - h5
45
+ - h6
46
+ - head
47
+ - header
48
+ - hgroup
49
+ - hr
50
+ - html
51
+ - i
52
+ - iframe
53
+ - img
54
+ - input
55
+ - ins
56
+ - kbd
57
+ - label
58
+ - legend
59
+ - li
60
+ - link
61
+ - main
62
+ - map
63
+ - mark
64
+ - menu
65
+ - meta
66
+ - meter
67
+ - nav
68
+ - noscript
69
+ - object
70
+ - ol
71
+ - optgroup
72
+ - option
73
+ - output
74
+ - p
75
+ - picture
76
+ - pre
77
+ - progress
78
+ - q
79
+ - rp
80
+ - rt
81
+ - ruby
82
+ - s
83
+ - samp
84
+ - script
85
+ - section
86
+ - select
87
+ - slot
88
+ - small
89
+ - source
90
+ - span
91
+ - strong
92
+ - style
93
+ - sub
94
+ - summary
95
+ - sup
96
+ - table
97
+ - tbody
98
+ - td
99
+ - template
100
+ - textarea
101
+ - tfoot
102
+ - th
103
+ - thead
104
+ - time
105
+ - title
106
+ - tr
107
+ - track
108
+ - u
109
+ - ul
110
+ - var
111
+ - video
112
+ - wbr
113
+ - animate
114
+ - animatemotion
115
+ - animatetransform
116
+ - circle
117
+ - clippath
118
+ - defs
119
+ - desc
120
+ - discard
121
+ - ellipse
122
+ - feblend
123
+ - fecolormatrix
124
+ - fecomponenttransfer
125
+ - fecomposite
126
+ - feconvolvematrix
127
+ - fediffuselighting
128
+ - fedisplacementmap
129
+ - fedistantlight
130
+ - fedropshadow
131
+ - feflood
132
+ - fefunca
133
+ - fefuncb
134
+ - fefuncg
135
+ - fefuncr
136
+ - fegaussianblur
137
+ - feimage
138
+ - femerge
139
+ - femergenode
140
+ - femorphology
141
+ - feoffset
142
+ - fepointlight
143
+ - fespecularlighting
144
+ - fespotlight
145
+ - fetile
146
+ - feturbulence
147
+ - filter
148
+ - foreignobject
149
+ - g
150
+ - image
151
+ - line
152
+ - lineargradient
153
+ - marker
154
+ - mask
155
+ - metadata
156
+ - mpath
157
+ - path
158
+ - pattern
159
+ - polygon
160
+ - polyline
161
+ - radialgradient
162
+ - rect
163
+ - script
164
+ - set
165
+ - stop
166
+ - svg
167
+ - switch
168
+ - symbol
169
+ - text
170
+ - textpath
171
+ - tspan
172
+ - use
173
+ - view
@@ -0,0 +1,75 @@
1
+
2
+ module Interfacets
3
+ module Test
4
+ class UiSimulator
5
+ TYPES = {
6
+ inline: Js::InlineBus,
7
+ nodo: Js::NodoBus,
8
+ mruby_wasm: Js::NodoBus,
9
+ }
10
+
11
+ attr_reader :system_json, :router, :type, :contract_path, :validation
12
+ def initialize(type:, bus:, router:, contract_path: nil, validation: :strict)
13
+ unless TYPES.keys.include?(type)
14
+ raise ArgumentError.new(
15
+ "type: #{type.inspect} not allowed. Must be one of #{TYPES.keys}"
16
+ )
17
+ end
18
+
19
+ @type = type
20
+ # Serialize and deserialize to ensure proper data structure for JS
21
+ @system_json = JSON.parse(bus.client_system_json(only_facets: type == :inline).to_json)
22
+ @router = router
23
+ @contract_path = contract_path
24
+ @validation = validation
25
+ end
26
+
27
+ def visit(path)
28
+ js.init(
29
+ hydrated_facet: router.call(path).render
30
+ )
31
+ end
32
+
33
+ def c(channel_id)
34
+ js.handler(channel_id)
35
+ end
36
+
37
+ def dispatch(channel_id, event)
38
+ js.dispatch(channel_id, event)
39
+ end
40
+
41
+ def url
42
+ c("url")
43
+ end
44
+
45
+ def dom
46
+ c("dom")
47
+ end
48
+
49
+ def timers
50
+ c("timer")
51
+ end
52
+
53
+ private
54
+
55
+ def js
56
+ @js ||= (
57
+ validation_engine = if contract_path
58
+ ValidationEngine.new(ComponentRegistry.load(contract_path), validation_mode: validation)
59
+ end
60
+
61
+ js_api = Test::Js::Receivers::Api.new(name: "interfacets:api", router:)
62
+ js_react = Test::Js::Receivers::React.new(name: "dom", validation_engine:)
63
+ js_url = Test::Js::Receivers::Url.new(name: "url")
64
+ js_timer = Test::Js::Receivers::Timer.new(name: "timer")
65
+ receivers = [js_api, js_react, js_url, js_timer]
66
+
67
+ TYPES.fetch(type).new(
68
+ receiver_index: receivers.map { [_1.name, _1] }.to_h,
69
+ client_system_json: system_json,
70
+ )
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json_schemer"
4
+ require "set"
5
+ require "yaml"
6
+
7
+ module Interfacets
8
+ module Test
9
+ class ValidationEngine
10
+ attr_reader :registry, :validation_mode
11
+
12
+ @warned_components = Set.new
13
+
14
+ def self.warned_components
15
+ @warned_components
16
+ end
17
+
18
+ def self.reset_warnings!
19
+ @warned_components.clear
20
+ end
21
+
22
+ def initialize(registry = ComponentRegistry.load, validation_mode: :strict)
23
+ @registry = registry
24
+ @validation_mode = validation_mode
25
+ end
26
+
27
+ def active?
28
+ registry.contracts.any?
29
+ end
30
+
31
+ def validate_props(component_name, props)
32
+ return unless active?
33
+
34
+ contract = registry.find_component(component_name)
35
+ is_standard = standard_element?(component_name)
36
+
37
+ props_config = contract&.dig("props")
38
+
39
+ if is_standard
40
+ return unless props_config
41
+ else
42
+ return unless contract_exists?(component_name)
43
+
44
+ raise ValidationError, "Missing props schema for custom component: #{component_name}" unless props_config
45
+ end
46
+
47
+ schema = { "type" => "object", "properties" => props_config }
48
+ validate!(schema, props, "properties for #{component_name}")
49
+ end
50
+
51
+ def validate_event(component_name, event_name, payload)
52
+ return unless active?
53
+
54
+ contract = registry.find_component(component_name)
55
+ is_standard = standard_element?(component_name)
56
+
57
+ event_config = contract&.dig("props", event_name.to_s)
58
+
59
+ if is_standard
60
+ return unless event_config
61
+ else
62
+ return unless contract_exists?(component_name)
63
+
64
+ raise ValidationError, "Missing events schema for custom component: #{component_name}" unless contract.key?("props")
65
+ raise ValidationError, "Missing event schema for '#{event_name}' in custom component: #{component_name}" unless event_config
66
+ end
67
+
68
+ schema = event_config.is_a?(Hash) ? (event_config["payload"] || {}) : {}
69
+
70
+ validate!(schema, payload, "event '#{event_name}' for #{component_name}")
71
+ end
72
+
73
+ private
74
+
75
+ def standard_element?(component_name)
76
+ @standard_elements ||= begin
77
+ yaml_path = File.expand_path("standard_elements.yml", __dir__)
78
+ Set.new(YAML.load_file(yaml_path).map(&:to_s))
79
+ end
80
+ @standard_elements.include?(component_name.to_s)
81
+ end
82
+
83
+ def contract_exists?(component_name)
84
+ contract = registry.find_component(component_name)
85
+ return true if contract
86
+
87
+ case validation_mode
88
+ when :strict
89
+ raise MissingComponentContractError, "Missing component contract for: #{component_name}"
90
+ when :warn
91
+ unless self.class.warned_components.include?(component_name)
92
+ warn "Warning: Missing component contract for: #{component_name}"
93
+ self.class.warned_components << component_name
94
+ end
95
+ false
96
+ when :permissive
97
+ false
98
+ else
99
+ raise ArgumentError, "Unknown validation_mode: #{validation_mode}"
100
+ end
101
+ end
102
+
103
+ def find_contract!(name)
104
+ contract = registry.find_component(name)
105
+ return contract if contract
106
+
107
+ raise MissingComponentContractError, "Missing component contract for: #{name}"
108
+ end
109
+
110
+ def validate!(schema, data, context)
111
+ full_schema = schema.dup
112
+ if full_schema["type"] == "object" && !full_schema.key?("additionalProperties")
113
+ full_schema["additionalProperties"] = false
114
+ end
115
+ full_schema["definitions"] = registry.definitions if registry.definitions.any?
116
+
117
+ schemer = JSONSchemer.schema(full_schema, keywords: {
118
+ "is_event" => lambda do |instance, keyword_value, pointer|
119
+ return true unless keyword_value
120
+ unless instance.is_a?(Hash) && instance.key?("type") && instance.key?("payload")
121
+ return [false, "missing 'type' or 'payload'"]
122
+ end
123
+
124
+ prop_name = pointer.split("/").last
125
+ prop_schema = full_schema.dig("properties", prop_name)
126
+ if prop_schema && (nested_schema = prop_schema["schema"])
127
+ nested_schemer = JSONSchemer.schema(nested_schema)
128
+ nested_errors = nested_schemer.validate(instance["payload"]).to_a
129
+ if nested_errors.any?
130
+ return [false, "event payload validation failed: #{nested_errors.map { |e| "#{e['data_pointer']}: #{e['type']} #{e['details']}" }.join(', ')}"]
131
+ end
132
+ end
133
+
134
+ true
135
+ end
136
+ })
137
+
138
+ errors = schemer.validate(data).to_a
139
+
140
+ return if errors.empty?
141
+
142
+ message = "Validation failed for #{context}:\n"
143
+ errors.each do |error|
144
+ message += " - #{error['data_pointer']}: #{error['type']} #{error['details']}\n"
145
+ end
146
+
147
+ raise ValidationError, message
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interfacets
4
+ module Test
5
+ module H
6
+ module_function
7
+
8
+ def j(hash)
9
+ JSON.parse(hash.to_json)
10
+ end
11
+ end
12
+ end
13
+ 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.99"
5
5
  end
data/lib/interfacets.rb CHANGED
@@ -1,8 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "interfacets/version"
3
+ require "zeitwerk"
4
+ require "active_support/concern"
4
5
 
5
6
  module Interfacets
7
+ class << self
8
+ def reload
9
+ loader.reload
10
+ end
11
+
12
+ def loader
13
+ @loader ||= (
14
+ Zeitwerk::Loader
15
+ .for_gem
16
+ .tap { _1.ignore("#{__dir__}/interfacets/mruby") }
17
+ .tap { _1.ignore("#{__dir__}/interfacets/client/utils") }
18
+ .tap { _1.enable_reloading if enable_reloading? }
19
+ .tap(&:setup)
20
+ )
21
+ end
22
+
23
+ def enable_reloading?
24
+ $interfacets_dev_mode
25
+ end
26
+ end
27
+
6
28
  class Error < StandardError; end
7
- # Your code goes here...
29
+ class ValidationError < Error; end
30
+ class MissingComponentContractError < Error; end
31
+ end
32
+
33
+ unless Interfacets.enable_reloading?
34
+ Interfacets.loader
8
35
  end