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,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
|
data/lib/interfacets/version.rb
CHANGED
data/lib/interfacets.rb
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
29
|
+
class ValidationError < Error; end
|
|
30
|
+
class MissingComponentContractError < Error; end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
unless Interfacets.enable_reloading?
|
|
34
|
+
Interfacets.loader
|
|
8
35
|
end
|