selenium-webdriver 4.46.0 → 4.48.0
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/CHANGES +39 -0
- data/bin/linux/selenium-manager +0 -0
- data/bin/macos/selenium-manager +0 -0
- data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +5940 -0
- data/bin/selenium-manager.cdx.json +11708 -0
- data/bin/windows/selenium-manager.exe +0 -0
- data/lib/selenium/webdriver/bidi/error.rb +48 -0
- data/lib/selenium/webdriver/bidi/protocol/bluetooth.rb +53 -23
- data/lib/selenium/webdriver/bidi/protocol/browser.rb +43 -15
- data/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +172 -68
- data/lib/selenium/webdriver/bidi/protocol/domain.rb +8 -2
- data/lib/selenium/webdriver/bidi/protocol/emulation.rb +123 -9
- data/lib/selenium/webdriver/bidi/protocol/error_code.rb +66 -0
- data/lib/selenium/webdriver/bidi/protocol/input.rb +101 -28
- data/lib/selenium/webdriver/bidi/protocol/log.rb +17 -6
- data/lib/selenium/webdriver/bidi/protocol/network.rb +146 -63
- data/lib/selenium/webdriver/bidi/protocol/permissions.rb +4 -2
- data/lib/selenium/webdriver/bidi/protocol/script.rb +264 -86
- data/lib/selenium/webdriver/bidi/protocol/session.rb +48 -3
- data/lib/selenium/webdriver/bidi/protocol/speculation.rb +4 -2
- data/lib/selenium/webdriver/bidi/protocol/storage.rb +32 -6
- data/lib/selenium/webdriver/bidi/protocol/user_agent_client_hints.rb +7 -2
- data/lib/selenium/webdriver/bidi/protocol/web_extension.rb +49 -10
- data/lib/selenium/webdriver/bidi/protocol.rb +3 -0
- data/lib/selenium/webdriver/bidi/serialization/record.rb +206 -33
- data/lib/selenium/webdriver/bidi/serialization/union.rb +50 -4
- data/lib/selenium/webdriver/bidi/serialization.rb +1 -1
- data/lib/selenium/webdriver/bidi/support/bidi_generate.rb +529 -45
- data/lib/selenium/webdriver/bidi/support/check_generated.rb +12 -4
- data/lib/selenium/webdriver/bidi/transport.rb +8 -3
- data/lib/selenium/webdriver/chrome/service.rb +4 -1
- data/lib/selenium/webdriver/chromium/driver.rb +0 -1
- data/lib/selenium/webdriver/chromium/options.rb +20 -0
- data/lib/selenium/webdriver/common/driver.rb +11 -0
- data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +0 -7
- data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +0 -7
- data/lib/selenium/webdriver/common/driver_finder.rb +4 -3
- data/lib/selenium/webdriver/common/options.rb +21 -1
- data/lib/selenium/webdriver/common/proxy.rb +0 -8
- data/lib/selenium/webdriver/common/selenium_manager.rb +2 -1
- data/lib/selenium/webdriver/common/takes_screenshot.rb +1 -1
- data/lib/selenium/webdriver/common/websocket_connection.rb +64 -26
- data/lib/selenium/webdriver/common.rb +0 -1
- data/lib/selenium/webdriver/edge/service.rb +4 -1
- data/lib/selenium/webdriver/firefox/driver.rb +0 -3
- data/lib/selenium/webdriver/firefox/options.rb +19 -0
- data/lib/selenium/webdriver/firefox/profile.rb +13 -6
- data/lib/selenium/webdriver/remote/bidi_bridge.rb +39 -12
- data/lib/selenium/webdriver/remote/bridge.rb +1 -1
- data/lib/selenium/webdriver/remote/driver.rb +3 -1
- data/lib/selenium/webdriver/remote/http/default.rb +1 -1
- data/lib/selenium/webdriver/safari/options.rb +16 -1
- data/lib/selenium/webdriver/support/guards/guard.rb +20 -3
- data/lib/selenium/webdriver/support/guards.rb +9 -3
- data/lib/selenium/webdriver/version.rb +1 -1
- metadata +6 -3
- data/lib/selenium/webdriver/common/driver_extensions/has_bidi.rb +0 -36
|
@@ -29,7 +29,8 @@ module Selenium
|
|
|
29
29
|
# @api private
|
|
30
30
|
class Record < ::Data
|
|
31
31
|
# Named Field, not Member, to avoid colliding with +::Data#members+.
|
|
32
|
-
Field = ::Data.define(:name, :wire_key, :nullable, :ref, :list, :fixed, :enum, :required, :primitive
|
|
32
|
+
Field = ::Data.define(:name, :wire_key, :nullable, :ref, :list, :fixed, :enum, :required, :primitive,
|
|
33
|
+
:scalar, :const)
|
|
33
34
|
|
|
34
35
|
def self.define(**spec)
|
|
35
36
|
extensible = spec.delete(:extensible) || false
|
|
@@ -59,7 +60,8 @@ module Selenium
|
|
|
59
60
|
Field.new(name: name.to_sym, wire_key: meta.fetch(:wire_key, name.to_s),
|
|
60
61
|
nullable: meta[:nullable] || false, ref: meta[:ref],
|
|
61
62
|
list: meta[:list] || false, fixed: meta.fetch(:fixed, UNSET), enum: meta[:enum],
|
|
62
|
-
required: meta.fetch(:required, true), primitive: meta[:primitive]
|
|
63
|
+
required: meta.fetch(:required, true), primitive: meta[:primitive],
|
|
64
|
+
scalar: meta[:scalar], const: meta.fetch(:const, UNSET))
|
|
63
65
|
end
|
|
64
66
|
private_class_method :field
|
|
65
67
|
|
|
@@ -77,18 +79,24 @@ module Selenium
|
|
|
77
79
|
construct(**attributes)
|
|
78
80
|
end
|
|
79
81
|
|
|
80
|
-
# Inbound: builds from the wire. A missing required field raises (in +wire_value+)
|
|
81
|
-
#
|
|
82
|
-
#
|
|
82
|
+
# Inbound: builds from the wire. A missing required field raises (in +wire_value+); enum
|
|
83
|
+
# tokens are mapped back to symbols and an unrecognized one raises (in +read+); an
|
|
84
|
+
# undeclared property is captured silently (extensible) or warned and dropped (closed)
|
|
85
|
+
# — strict on shape, lenient on extras.
|
|
83
86
|
def from_json(json_payload)
|
|
84
87
|
unless json_payload.is_a?(::Hash)
|
|
85
|
-
raise Error::
|
|
88
|
+
raise Error::SerializationError, "#{name} expected an object on the wire, got #{json_payload.inspect}"
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
attributes = fields.to_h do |f|
|
|
89
92
|
[f.name, wire_value(f, json_payload)]
|
|
90
93
|
end
|
|
91
|
-
|
|
94
|
+
undeclared = extra(json_payload)
|
|
95
|
+
if extensible?
|
|
96
|
+
attributes[:extensions] = undeclared # the spec sanctions these extras; preserve them silently
|
|
97
|
+
else
|
|
98
|
+
warn_undeclared(undeclared) unless undeclared.empty?
|
|
99
|
+
end
|
|
92
100
|
construct(**attributes)
|
|
93
101
|
end
|
|
94
102
|
|
|
@@ -96,9 +104,11 @@ module Selenium
|
|
|
96
104
|
|
|
97
105
|
# Checks each field's value: a required field cannot be omitted (UNSET), a non-nullable
|
|
98
106
|
# field cannot be nil (nil is neither a value nor the UNSET omit-sentinel, so it would be
|
|
99
|
-
# silently dropped on the wire),
|
|
100
|
-
#
|
|
101
|
-
#
|
|
107
|
+
# silently dropped on the wire), a nullable-const field must carry its literal (not some
|
|
108
|
+
# other value), a primitive field must be the matching Ruby type, and an enum field must be
|
|
109
|
+
# in its allowed set. The enum constant is resolved lazily so a cross-domain enum need not be
|
|
110
|
+
# loaded first. Outbound only (from +new+); inbound presence/primitive/enum are checked
|
|
111
|
+
# separately in +wire_value+/+read+.
|
|
102
112
|
def validate_values(attributes)
|
|
103
113
|
fields.each do |f|
|
|
104
114
|
value = attributes[f.name]
|
|
@@ -106,11 +116,79 @@ module Selenium
|
|
|
106
116
|
raise ::ArgumentError, "#{name}##{f.name} cannot be nil" if value.nil? && !f.nullable
|
|
107
117
|
next if value.nil? || UNSET.equal?(value)
|
|
108
118
|
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
validate_present(f, value)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Checks a field that carries an actual value (neither omitted nor nil): a nullable-const
|
|
124
|
+
# field against its literal, list/scalar shape, primitive type (lists excepted, as inbound
|
|
125
|
+
# does), ref type, and enum membership (resolved lazily so a cross-domain enum need not load first).
|
|
126
|
+
def validate_present(field, value)
|
|
127
|
+
validate_const(field, value)
|
|
128
|
+
check_outbound_shape(field, value)
|
|
129
|
+
check_outbound_primitive(field, value) unless field.list
|
|
130
|
+
validate_ref(field, value) if field.ref
|
|
131
|
+
Serialization.validate!("#{name}##{field.name}", value, Protocol.const_get(field.enum)) if field.enum
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Outbound mirror of read_ref: a ref-typed value must be the type it declares, so a wrong
|
|
135
|
+
# record or a value no union variant accepts is a caller error caught here, not a browser
|
|
136
|
+
# round-trip. Shape is already checked, so a list is an Array.
|
|
137
|
+
def validate_ref(field, value)
|
|
138
|
+
klass = (@refs ||= {})[field.name] ||= Protocol.const_get(field.ref)
|
|
139
|
+
field.list ? validate_ref_list(field, klass, value) : validate_ref_value(field, klass, value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Mirrors read_list: a scalar field is a [key, value] map, a nested list recurses, otherwise
|
|
143
|
+
# each element is checked against the ref.
|
|
144
|
+
def validate_ref_list(field, klass, list)
|
|
145
|
+
list.each do |element|
|
|
146
|
+
if field.scalar
|
|
147
|
+
validate_ref_entry(field, klass, element)
|
|
148
|
+
elsif element.is_a?(::Array)
|
|
149
|
+
validate_ref_list(field, klass, element)
|
|
150
|
+
else
|
|
151
|
+
validate_ref_value(field, klass, element)
|
|
152
|
+
end
|
|
111
153
|
end
|
|
112
154
|
end
|
|
113
155
|
|
|
156
|
+
# A [key, value] map entry: the key may be a variant or a bare scalar, the value is a variant.
|
|
157
|
+
def validate_ref_entry(field, klass, element)
|
|
158
|
+
unless element.is_a?(::Array) && element.size == 2
|
|
159
|
+
raise ::ArgumentError, "#{name}##{field.name} expected a [key, value] pair, got #{element.inspect}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
key, value = element
|
|
163
|
+
key.is_a?(Serializable) ? validate_ref_value(field, klass, key) : check_outbound_scalar(field, key)
|
|
164
|
+
validate_ref_value(field, klass, value)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# A record ref must be an instance of that record; a union ref must be one the union accepts.
|
|
168
|
+
def validate_ref_value(field, klass, value)
|
|
169
|
+
return if klass < Union ? klass.valid_outbound?(value) : value.is_a?(klass)
|
|
170
|
+
|
|
171
|
+
raise ::ArgumentError, "#{name}##{field.name} expected #{field.ref}, got #{value.inspect}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Outbound mirror of scalar_value: a bare map key must match one of the arm's primitives.
|
|
175
|
+
def check_outbound_scalar(field, value)
|
|
176
|
+
checks = Array(field.scalar).filter_map { |primitive| PRIMITIVE_CHECKS[primitive] }
|
|
177
|
+
return if checks.empty? || checks.any? { |check| check.call(value) }
|
|
178
|
+
|
|
179
|
+
raise ::ArgumentError,
|
|
180
|
+
"#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A nullable constant (`literal / null`) is caller-settable but its only non-null value is
|
|
184
|
+
# the literal, so a value that is neither the literal nor nil (nil is handled above) is a
|
|
185
|
+
# local error rather than a wire round-trip. A non-const field carries UNSET here and passes.
|
|
186
|
+
def validate_const(field, value)
|
|
187
|
+
return if UNSET.equal?(field.const) || value == field.const
|
|
188
|
+
|
|
189
|
+
raise ::ArgumentError, "#{name}##{field.name} must be #{field.const.inspect}, got #{value.inspect}"
|
|
190
|
+
end
|
|
191
|
+
|
|
114
192
|
# Outbound mirror of check_shape: a list-typed arg must be an array, a scalar-shaped one
|
|
115
193
|
# (enum or ref, not a list) must not — a local ArgumentError, not a wire round-trip.
|
|
116
194
|
def check_outbound_shape(field, value)
|
|
@@ -121,34 +199,61 @@ module Selenium
|
|
|
121
199
|
raise ::ArgumentError, "#{name}##{field.name} expected #{kind}, got #{value.inspect}"
|
|
122
200
|
end
|
|
123
201
|
|
|
202
|
+
# Outbound mirror of check_primitive: a primitive-typed arg (`string`/`integer`/…) must be
|
|
203
|
+
# the matching Ruby type, so a caller mistake (a string width, a float count) is a local
|
|
204
|
+
# ArgumentError here rather than a rejection the browser reports a round-trip later. A field
|
|
205
|
+
# with no primitive descriptor (enum, ref, opaque) passes; lists are skipped, as inbound does.
|
|
206
|
+
def check_outbound_primitive(field, value)
|
|
207
|
+
check = PRIMITIVE_CHECKS[field.primitive]
|
|
208
|
+
return if check.nil? || check.call(value)
|
|
209
|
+
|
|
210
|
+
raise ::ArgumentError, "#{name}##{field.name} expected #{field.primitive}, got #{value.inspect}"
|
|
211
|
+
end
|
|
212
|
+
|
|
124
213
|
def fixed?(field)
|
|
125
214
|
!UNSET.equal?(field.fixed)
|
|
126
215
|
end
|
|
127
216
|
|
|
217
|
+
# A required field absent from the response cannot yield a valid typed object, so it raises
|
|
218
|
+
# rather than substitute a placeholder or represent the field as omitted; a remote end that
|
|
219
|
+
# lags the schema is handled by a project schema override, not by runtime tolerance.
|
|
128
220
|
def wire_value(field, json_payload)
|
|
129
221
|
return field.fixed if fixed?(field)
|
|
130
222
|
return read(field, json_payload[field.wire_key]) if json_payload.key?(field.wire_key)
|
|
131
223
|
return UNSET unless field.required
|
|
132
224
|
|
|
133
|
-
raise Error::
|
|
225
|
+
raise Error::SerializationError, "#{name}##{field.name} is required but was missing from the response"
|
|
134
226
|
end
|
|
135
227
|
|
|
136
228
|
def read(field, raw)
|
|
137
229
|
if raw.nil?
|
|
138
230
|
return raw if field.nullable
|
|
139
231
|
|
|
140
|
-
raise Error::
|
|
232
|
+
raise Error::SerializationError, "#{name}##{field.name} received null but is not nullable"
|
|
141
233
|
end
|
|
142
234
|
check_shape(field, raw)
|
|
143
235
|
return Serialization.to_symbol("#{name}##{field.name}", raw, enum_hash(field)) if field.enum
|
|
144
236
|
|
|
145
237
|
if field.ref.nil?
|
|
146
|
-
|
|
147
|
-
|
|
238
|
+
return raw if field.list
|
|
239
|
+
|
|
240
|
+
check_primitive(field, raw)
|
|
241
|
+
# A whole number is exact in both types, so the declared type is held with nothing lost.
|
|
242
|
+
return field.primitive == 'integer' && raw.is_a?(::Float) ? raw.to_i : raw
|
|
148
243
|
end
|
|
149
244
|
|
|
245
|
+
read_ref(field, raw)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Reads a ref-typed value into its class. A `scalar` position is an inline union with
|
|
249
|
+
# a scalar arm collapsed onto its union ref (a map's string keys): a non-object leaf
|
|
250
|
+
# passes through instead of being handed to the object_only union, but only when it
|
|
251
|
+
# matches the arm's primitive (+scalar+ carries it). A list recurses per element.
|
|
252
|
+
def read_ref(field, raw)
|
|
150
253
|
klass = (@refs ||= {})[field.name] ||= Protocol.const_get(field.ref)
|
|
151
|
-
|
|
254
|
+
return read_list(field, raw, klass) if field.list
|
|
255
|
+
|
|
256
|
+
field.scalar && !raw.is_a?(::Hash) ? scalar_value(field, raw) : klass.from_json(raw)
|
|
152
257
|
end
|
|
153
258
|
|
|
154
259
|
# A declared list must arrive as an array; a scalar-shaped field (enum or ref, not a
|
|
@@ -157,40 +262,91 @@ module Selenium
|
|
|
157
262
|
return if field.list == raw.is_a?(::Array)
|
|
158
263
|
return unless field.list || field.enum || field.ref
|
|
159
264
|
|
|
160
|
-
raise Error::
|
|
265
|
+
raise Error::SerializationError,
|
|
161
266
|
"#{name}##{field.name} expected #{field.list ? 'a list' : 'a single value'}, got #{raw.inspect}"
|
|
162
267
|
end
|
|
163
268
|
|
|
164
|
-
#
|
|
165
|
-
# number type)
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
'
|
|
269
|
+
# The check a schema primitive admits, by JSON kind rather than Ruby class: `number` is
|
|
270
|
+
# any Numeric (JSON has one number type), and `integer` is any whole one — a browser is
|
|
271
|
+
# free to send `5` or `5.0` (JS has no int/float split), while a fractional value like
|
|
272
|
+
# 1.5 is a real mismatch. A field with no primitive descriptor is left unchecked.
|
|
273
|
+
WHOLE_FLOAT = ->(value) { value.is_a?(::Float) && value.finite? && (value % 1).zero? }
|
|
274
|
+
PRIMITIVE_CHECKS = {
|
|
275
|
+
'string' => ->(value) { value.is_a?(::String) },
|
|
276
|
+
'boolean' => ->(value) { value.is_a?(::TrueClass) || value.is_a?(::FalseClass) },
|
|
277
|
+
'number' => ->(value) { value.is_a?(::Numeric) },
|
|
278
|
+
'integer' => ->(value) { value.is_a?(::Integer) || WHOLE_FLOAT.call(value) }
|
|
171
279
|
}.freeze
|
|
172
280
|
|
|
173
281
|
def check_primitive(field, raw)
|
|
174
|
-
|
|
175
|
-
return if
|
|
282
|
+
check = PRIMITIVE_CHECKS[field.primitive]
|
|
283
|
+
return if check.nil? || check.call(raw)
|
|
176
284
|
|
|
177
|
-
raise Error::
|
|
285
|
+
raise Error::SerializationError, "#{name}##{field.name} expected #{field.primitive}, got #{raw.inspect}"
|
|
178
286
|
end
|
|
179
287
|
|
|
180
288
|
def enum_hash(field)
|
|
181
289
|
(@enums ||= {})[field.name] ||= Protocol.const_get(field.enum)
|
|
182
290
|
end
|
|
183
291
|
|
|
184
|
-
# Parses each element
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
292
|
+
# Parses each element. A `scalar` field is a map encoded as `[key, value]` pairs, so
|
|
293
|
+
# every element must be a 2-item pair — each is read as one, and a malformed entry is
|
|
294
|
+
# rejected. Non-scalar lists recurse into nested lists; other elements deserialize.
|
|
295
|
+
def read_list(field, raw, klass)
|
|
296
|
+
raw.map do |element|
|
|
297
|
+
if field.scalar
|
|
298
|
+
read_map_entry(field, element, klass)
|
|
299
|
+
elsif element.is_a?(::Array)
|
|
300
|
+
read_list(field, element, klass)
|
|
301
|
+
else
|
|
302
|
+
klass.from_json(element)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# A map entry is a `[key, value]` pair. The key is `Ref / text` — an object key
|
|
308
|
+
# deserializes, a bare-string key passes through once validated against the arm's
|
|
309
|
+
# primitive. The value is the object-only Ref and always deserializes, so a bare scalar
|
|
310
|
+
# there is rejected (object_only holds at the value position). A non-pair element is a
|
|
311
|
+
# malformed entry and is rejected outright.
|
|
312
|
+
def read_map_entry(field, element, klass)
|
|
313
|
+
unless element.is_a?(::Array) && element.size == 2
|
|
314
|
+
raise Error::SerializationError,
|
|
315
|
+
"#{name}##{field.name} expected a [key, value] pair, got #{element.inspect}"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
key, value = element
|
|
319
|
+
key = key.is_a?(::Hash) ? klass.from_json(key) : scalar_value(field, key)
|
|
320
|
+
[key, klass.from_json(value)]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# A bare scalar at a scalar-tolerant union position must match one of the union's
|
|
324
|
+
# scalar-arm primitives (+scalar+ is a primitive name or an array of them); a
|
|
325
|
+
# wrong-typed scalar (a number where a string is expected) is a wire error, not
|
|
326
|
+
# something to pass through. An unrecognized primitive (none in PRIMITIVE_CHECKS) is
|
|
327
|
+
# left unchecked, matching the lenient default elsewhere.
|
|
328
|
+
def scalar_value(field, value)
|
|
329
|
+
checks = Array(field.scalar).filter_map { |primitive| PRIMITIVE_CHECKS[primitive] }
|
|
330
|
+
return value if checks.empty? || checks.any? { |check| check.call(value) }
|
|
331
|
+
|
|
332
|
+
raise Error::SerializationError,
|
|
333
|
+
"#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}"
|
|
188
334
|
end
|
|
189
335
|
|
|
190
336
|
def extra(json_payload)
|
|
191
337
|
known = (@wire_keys ||= fields.map(&:wire_key))
|
|
192
338
|
json_payload.except(*known)
|
|
193
339
|
end
|
|
340
|
+
|
|
341
|
+
# Forward-compat signal: a property a closed type does not model is dropped and warned so
|
|
342
|
+
# schema drift is visible (an extensible type keeps its extras silently — the spec sanctions
|
|
343
|
+
# them). Tagged +:bidi_undeclared_property+ so a caller can silence it via +logger.ignore+.
|
|
344
|
+
def warn_undeclared(undeclared)
|
|
345
|
+
undeclared.each_key do |key|
|
|
346
|
+
WebDriver.logger.warn("#{name} received an undeclared property: #{key.inspect}",
|
|
347
|
+
id: :bidi_undeclared_property)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
194
350
|
end
|
|
195
351
|
|
|
196
352
|
# @api private
|
|
@@ -215,9 +371,26 @@ module Selenium
|
|
|
215
371
|
value = Serialization.to_wire(value, Protocol.const_get(f.enum)) if f.enum
|
|
216
372
|
payload[f.wire_key] = Serializable.as_json(value)
|
|
217
373
|
end
|
|
218
|
-
|
|
374
|
+
merge_extensions!(payload) if self.class.extensible? && !extensions.empty?
|
|
219
375
|
payload
|
|
220
376
|
end
|
|
377
|
+
|
|
378
|
+
private
|
|
379
|
+
|
|
380
|
+
# Merge the passthrough extras onto the wire, erroring rather than letting an extra whose key
|
|
381
|
+
# is a declared field's wire key silently clobber that typed value; an extra is by definition
|
|
382
|
+
# a field the spec does not declare. Keys are stringified first so a symbol key (e.g. `name:`)
|
|
383
|
+
# cannot slip past the guard and then reappear as a duplicate wire key once serialized. The
|
|
384
|
+
# single gate every outbound path funnels through: +new+, +with+, and in-place mutation.
|
|
385
|
+
def merge_extensions!(payload)
|
|
386
|
+
extras = extensions.transform_keys(&:to_s)
|
|
387
|
+
collisions = extras.keys & self.class.fields.map(&:wire_key)
|
|
388
|
+
unless collisions.empty?
|
|
389
|
+
raise ::ArgumentError, "#{self.class.name} extensions shadow declared fields: #{collisions.join(', ')}"
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
payload.merge!(extras)
|
|
393
|
+
end
|
|
221
394
|
end
|
|
222
395
|
end
|
|
223
396
|
end
|
|
@@ -44,14 +44,32 @@ module Selenium
|
|
|
44
44
|
def presence(rules) = @presence = rules
|
|
45
45
|
def fallback(path) = @fallback = path
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
47
|
+
# Declared (via the schema's `objectOnly` signal) on a union whose every arm is an
|
|
48
|
+
# object, so a non-Hash payload is a schema violation rather than a scalar arm.
|
|
49
|
+
def object_only = @object_only = true
|
|
50
|
+
|
|
51
|
+
# Declared (via the schema's `scalarValues` signal) on a non-object_only union whose
|
|
52
|
+
# bare-scalar arms are a fixed set of literals (input.Origin's "viewport" / "pointer").
|
|
53
|
+
# An outbound scalar outside that set matches no arm, so it is a caller error.
|
|
54
|
+
def scalar_values(*values) = @scalar_values = values
|
|
55
|
+
|
|
56
|
+
# A non-Hash payload is a bare scalar arm (e.g. input.Origin's "viewport"), valid only
|
|
57
|
+
# as a literal the schema pins; under object_only it cannot match any variant at all.
|
|
49
58
|
def from_json(json_payload)
|
|
50
|
-
|
|
59
|
+
unless json_payload.is_a?(::Hash)
|
|
60
|
+
if @object_only
|
|
61
|
+
raise Error::SerializationError,
|
|
62
|
+
"#{name} expected an object on the wire, got #{json_payload.inspect}"
|
|
63
|
+
end
|
|
64
|
+
return json_payload if scalar_arm?(json_payload)
|
|
65
|
+
|
|
66
|
+
raise Error::SerializationError,
|
|
67
|
+
"#{name} received a scalar not in this Selenium's BiDi schema: #{json_payload.inspect}"
|
|
68
|
+
end
|
|
51
69
|
|
|
52
70
|
variant = select(json_payload)
|
|
53
71
|
unless variant
|
|
54
|
-
raise Error::
|
|
72
|
+
raise Error::SerializationError,
|
|
55
73
|
"#{name} received a variant not in this Selenium's BiDi schema: #{json_payload.inspect}"
|
|
56
74
|
end
|
|
57
75
|
Protocol.const_get(variant).from_json(json_payload)
|
|
@@ -75,8 +93,36 @@ module Selenium
|
|
|
75
93
|
raise ::ArgumentError, "invalid combination for #{name}: #{invalid.join(', ')}"
|
|
76
94
|
end
|
|
77
95
|
|
|
96
|
+
# Outbound mirror of from_json: is +value+ one this union accepts? Any variant is accepted,
|
|
97
|
+
# and a variant that is itself a union recurses (e.g. LocalValue's RemoteReference fallback).
|
|
98
|
+
# A non-object_only union (e.g. input.Origin) also admits one of its pinned bare-scalar
|
|
99
|
+
# literals; an object (a Hash or another union's record) that matched no variant does not.
|
|
100
|
+
def valid_outbound?(value)
|
|
101
|
+
return true if variant_refs.any? { |ref| variant_accepts?(ref, value) }
|
|
102
|
+
|
|
103
|
+
!@object_only && scalar_arm?(value)
|
|
104
|
+
end
|
|
105
|
+
|
|
78
106
|
private
|
|
79
107
|
|
|
108
|
+
# A bare-scalar arm must be one of the literals the schema pinned for this union
|
|
109
|
+
# (scalar_values, e.g. input.Origin's "viewport" / "pointer"). The generator guarantees a
|
|
110
|
+
# non-object_only union declares them, so no runtime guard is needed here.
|
|
111
|
+
def scalar_arm?(value)
|
|
112
|
+
@scalar_values.include?(value)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Every variant's class name: the discriminated table, the presence paths, and the fallback.
|
|
116
|
+
def variant_refs
|
|
117
|
+
@variant_refs ||= [*@variants&.values, *@presence&.keys, @fallback].compact
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A variant that is itself a union recurses; a record variant is matched by instance.
|
|
121
|
+
def variant_accepts?(ref, value)
|
|
122
|
+
klass = (@variant_classes ||= {})[ref] ||= Protocol.const_get(ref)
|
|
123
|
+
klass < Union ? klass.valid_outbound?(value) : value.is_a?(klass)
|
|
124
|
+
end
|
|
125
|
+
|
|
80
126
|
# The discriminator value may legitimately be null (e.g. script.NullValue's
|
|
81
127
|
# "null" tag), so it is matched by key presence.
|
|
82
128
|
def select(json_payload)
|
|
@@ -67,7 +67,7 @@ module Selenium
|
|
|
67
67
|
return value if value.nil?
|
|
68
68
|
return value.map { |element| to_symbol(name, element, enum) } if value.is_a?(::Array)
|
|
69
69
|
|
|
70
|
-
enum.key(value) || raise(Error::
|
|
70
|
+
enum.key(value) || raise(Error::SerializationError, "#{name} received an unknown value: #{value.inspect}")
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
end # BiDi
|