selenium-webdriver 4.46.0 → 4.47.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +29 -0
  3. data/bin/linux/selenium-manager +0 -0
  4. data/bin/macos/selenium-manager +0 -0
  5. data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +5971 -0
  6. data/bin/selenium-manager.cdx.json +11629 -0
  7. data/bin/windows/selenium-manager.exe +0 -0
  8. data/lib/selenium/webdriver/{common/driver_extensions/has_bidi.rb → bidi/error.rb} +21 -13
  9. data/lib/selenium/webdriver/bidi/protocol/bluetooth.rb +53 -23
  10. data/lib/selenium/webdriver/bidi/protocol/browser.rb +43 -15
  11. data/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +172 -68
  12. data/lib/selenium/webdriver/bidi/protocol/domain.rb +8 -2
  13. data/lib/selenium/webdriver/bidi/protocol/emulation.rb +123 -9
  14. data/lib/selenium/webdriver/bidi/protocol/error_code.rb +66 -0
  15. data/lib/selenium/webdriver/bidi/protocol/input.rb +101 -28
  16. data/lib/selenium/webdriver/bidi/protocol/log.rb +17 -6
  17. data/lib/selenium/webdriver/bidi/protocol/network.rb +146 -63
  18. data/lib/selenium/webdriver/bidi/protocol/permissions.rb +4 -2
  19. data/lib/selenium/webdriver/bidi/protocol/script.rb +264 -86
  20. data/lib/selenium/webdriver/bidi/protocol/session.rb +48 -3
  21. data/lib/selenium/webdriver/bidi/protocol/speculation.rb +4 -2
  22. data/lib/selenium/webdriver/bidi/protocol/storage.rb +32 -6
  23. data/lib/selenium/webdriver/bidi/protocol/user_agent_client_hints.rb +7 -2
  24. data/lib/selenium/webdriver/bidi/protocol/web_extension.rb +49 -10
  25. data/lib/selenium/webdriver/bidi/protocol.rb +3 -0
  26. data/lib/selenium/webdriver/bidi/serialization/record.rb +194 -18
  27. data/lib/selenium/webdriver/bidi/serialization/union.rb +45 -3
  28. data/lib/selenium/webdriver/bidi/serialization.rb +13 -1
  29. data/lib/selenium/webdriver/bidi/support/bidi_generate.rb +529 -45
  30. data/lib/selenium/webdriver/bidi/support/check_generated.rb +12 -4
  31. data/lib/selenium/webdriver/bidi/transport.rb +8 -3
  32. data/lib/selenium/webdriver/chrome/service.rb +4 -1
  33. data/lib/selenium/webdriver/chromium/driver.rb +0 -1
  34. data/lib/selenium/webdriver/chromium/options.rb +20 -0
  35. data/lib/selenium/webdriver/common/driver.rb +11 -0
  36. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +0 -7
  37. data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +0 -7
  38. data/lib/selenium/webdriver/common/driver_finder.rb +4 -3
  39. data/lib/selenium/webdriver/common/options.rb +21 -1
  40. data/lib/selenium/webdriver/common/proxy.rb +0 -8
  41. data/lib/selenium/webdriver/common/selenium_manager.rb +2 -1
  42. data/lib/selenium/webdriver/common/takes_screenshot.rb +1 -1
  43. data/lib/selenium/webdriver/common.rb +0 -1
  44. data/lib/selenium/webdriver/edge/service.rb +4 -1
  45. data/lib/selenium/webdriver/firefox/driver.rb +0 -3
  46. data/lib/selenium/webdriver/firefox/options.rb +19 -0
  47. data/lib/selenium/webdriver/firefox/profile.rb +13 -6
  48. data/lib/selenium/webdriver/remote/bidi_bridge.rb +39 -12
  49. data/lib/selenium/webdriver/remote/bridge.rb +1 -1
  50. data/lib/selenium/webdriver/remote/driver.rb +3 -1
  51. data/lib/selenium/webdriver/remote/http/default.rb +1 -1
  52. data/lib/selenium/webdriver/safari/options.rb +16 -1
  53. data/lib/selenium/webdriver/support/guards/guard.rb +20 -3
  54. data/lib/selenium/webdriver/support/guards.rb +9 -3
  55. data/lib/selenium/webdriver/version.rb +1 -1
  56. metadata +6 -3
@@ -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,9 +79,10 @@ 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
- # enum tokens are mapped back to symbols and an unrecognized one raises (in +read+), and
82
- # extra keys are captured (extensible) or ignored (closed) strict on shape, lenient on extras.
82
+ # Inbound: builds from the wire. A missing required field is omitted and warned (or
83
+ # raised in strict mode, in +wire_value+); enum tokens are mapped back to symbols and an
84
+ # unrecognized one raises (in +read+); an undeclared property is captured silently
85
+ # (extensible) or warned and dropped (closed) — strict on shape, lenient on extras.
83
86
  def from_json(json_payload)
84
87
  unless json_payload.is_a?(::Hash)
85
88
  raise Error::WebDriverError, "#{name} expected an object on the wire, got #{json_payload.inspect}"
@@ -88,7 +91,12 @@ module Selenium
88
91
  attributes = fields.to_h do |f|
89
92
  [f.name, wire_value(f, json_payload)]
90
93
  end
91
- attributes[:extensions] = extra(json_payload) if extensible?
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), and an enum field must be in its allowed set. The enum
100
- # constant is resolved lazily so a cross-domain enum need not be loaded first. Outbound
101
- # only (from +new+); inbound presence/enum are checked separately in +wire_value+/+read+.
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
- check_outbound_shape(f, value)
110
- Serialization.validate!("#{name}##{f.name}", value, Protocol.const_get(f.enum)) if f.enum
119
+ validate_present(f, value)
111
120
  end
112
121
  end
113
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
153
+ end
154
+ end
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
+ expected = Array(field.scalar).flat_map { |primitive| PRIMITIVE_TYPES[primitive] || [] }
177
+ return if expected.empty? || expected.any? { |type| value.is_a?(type) }
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,6 +199,17 @@ 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
+ expected = PRIMITIVE_TYPES[field.primitive]
208
+ return if expected.nil? || expected.any? { |type| value.is_a?(type) }
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
@@ -130,7 +219,19 @@ module Selenium
130
219
  return read(field, json_payload[field.wire_key]) if json_payload.key?(field.wire_key)
131
220
  return UNSET unless field.required
132
221
 
133
- raise Error::WebDriverError, "#{name}##{field.name} is required but was missing from the response"
222
+ missing_required(field)
223
+ end
224
+
225
+ # A required field absent from the response is tolerated as omitted (UNSET) and warned, so a
226
+ # schema ahead of the browser does not block the caller; strict mode (SE_BIDI_STRICT) escalates
227
+ # to an error for callers who want it. Omitted (UNSET) stays distinct from an explicit null (nil),
228
+ # which matters for the required-and-nullable fields the schema flags.
229
+ def missing_required(field)
230
+ message = "#{name}##{field.name} is required but was missing from the response"
231
+ raise Error::WebDriverError, message if Serialization.strict?
232
+
233
+ WebDriver.logger.warn(message, id: :bidi_missing_required)
234
+ UNSET
134
235
  end
135
236
 
136
237
  def read(field, raw)
@@ -147,8 +248,18 @@ module Selenium
147
248
  return raw
148
249
  end
149
250
 
251
+ read_ref(field, raw)
252
+ end
253
+
254
+ # Reads a ref-typed value into its class. A `scalar` position is an inline union with
255
+ # a scalar arm collapsed onto its union ref (a map's string keys): a non-object leaf
256
+ # passes through instead of being handed to the object_only union, but only when it
257
+ # matches the arm's primitive (+scalar+ carries it). A list recurses per element.
258
+ def read_ref(field, raw)
150
259
  klass = (@refs ||= {})[field.name] ||= Protocol.const_get(field.ref)
151
- field.list ? read_list(raw, klass) : klass.from_json(raw)
260
+ return read_list(field, raw, klass) if field.list
261
+
262
+ field.scalar && !raw.is_a?(::Hash) ? scalar_value(field, raw) : klass.from_json(raw)
152
263
  end
153
264
 
154
265
  # A declared list must arrive as an array; a scalar-shaped field (enum or ref, not a
@@ -181,16 +292,64 @@ module Selenium
181
292
  (@enums ||= {})[field.name] ||= Protocol.const_get(field.enum)
182
293
  end
183
294
 
184
- # Parses each element, recursing into nested lists (e.g. a map's [key, value] pairs)
185
- # so their entries become typed too.
186
- def read_list(raw, klass)
187
- raw.map { |element| element.is_a?(::Array) ? read_list(element, klass) : klass.from_json(element) }
295
+ # Parses each element. A `scalar` field is a map encoded as `[key, value]` pairs, so
296
+ # every element must be a 2-item pair — each is read as one, and a malformed entry is
297
+ # rejected. Non-scalar lists recurse into nested lists; other elements deserialize.
298
+ def read_list(field, raw, klass)
299
+ raw.map do |element|
300
+ if field.scalar
301
+ read_map_entry(field, element, klass)
302
+ elsif element.is_a?(::Array)
303
+ read_list(field, element, klass)
304
+ else
305
+ klass.from_json(element)
306
+ end
307
+ end
308
+ end
309
+
310
+ # A map entry is a `[key, value]` pair. The key is `Ref / text` — an object key
311
+ # deserializes, a bare-string key passes through once validated against the arm's
312
+ # primitive. The value is the object-only Ref and always deserializes, so a bare scalar
313
+ # there is rejected (object_only holds at the value position). A non-pair element is a
314
+ # malformed entry and is rejected outright.
315
+ def read_map_entry(field, element, klass)
316
+ unless element.is_a?(::Array) && element.size == 2
317
+ raise Error::WebDriverError,
318
+ "#{name}##{field.name} expected a [key, value] pair, got #{element.inspect}"
319
+ end
320
+
321
+ key, value = element
322
+ key = key.is_a?(::Hash) ? klass.from_json(key) : scalar_value(field, key)
323
+ [key, klass.from_json(value)]
324
+ end
325
+
326
+ # A bare scalar at a scalar-tolerant union position must match one of the union's
327
+ # scalar-arm primitives (+scalar+ is a primitive name or an array of them); a
328
+ # wrong-typed scalar (a number where a string is expected) is a wire error, not
329
+ # something to pass through. An unrecognized primitive (none in PRIMITIVE_TYPES) is
330
+ # left unchecked, matching the lenient default elsewhere.
331
+ def scalar_value(field, value)
332
+ expected = Array(field.scalar).flat_map { |primitive| PRIMITIVE_TYPES[primitive] || [] }
333
+ return value if expected.empty? || expected.any? { |type| value.is_a?(type) }
334
+
335
+ raise Error::WebDriverError,
336
+ "#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}"
188
337
  end
189
338
 
190
339
  def extra(json_payload)
191
340
  known = (@wire_keys ||= fields.map(&:wire_key))
192
341
  json_payload.except(*known)
193
342
  end
343
+
344
+ # Forward-compat signal: a property a closed type does not model is dropped and warned so
345
+ # schema drift is visible (an extensible type keeps its extras silently — the spec sanctions
346
+ # them). Tagged +:bidi_undeclared_property+ so a caller can silence it via +logger.ignore+.
347
+ def warn_undeclared(undeclared)
348
+ undeclared.each_key do |key|
349
+ WebDriver.logger.warn("#{name} received an undeclared property: #{key.inspect}",
350
+ id: :bidi_undeclared_property)
351
+ end
352
+ end
194
353
  end
195
354
 
196
355
  # @api private
@@ -215,9 +374,26 @@ module Selenium
215
374
  value = Serialization.to_wire(value, Protocol.const_get(f.enum)) if f.enum
216
375
  payload[f.wire_key] = Serializable.as_json(value)
217
376
  end
218
- payload.merge!(extensions) if self.class.extensible? && !extensions.empty?
377
+ merge_extensions!(payload) if self.class.extensible? && !extensions.empty?
219
378
  payload
220
379
  end
380
+
381
+ private
382
+
383
+ # Merge the passthrough extras onto the wire, erroring rather than letting an extra whose key
384
+ # is a declared field's wire key silently clobber that typed value; an extra is by definition
385
+ # a field the spec does not declare. Keys are stringified first so a symbol key (e.g. `name:`)
386
+ # cannot slip past the guard and then reappear as a duplicate wire key once serialized. The
387
+ # single gate every outbound path funnels through: +new+, +with+, and in-place mutation.
388
+ def merge_extensions!(payload)
389
+ extras = extensions.transform_keys(&:to_s)
390
+ collisions = extras.keys & self.class.fields.map(&:wire_key)
391
+ unless collisions.empty?
392
+ raise ::ArgumentError, "#{self.class.name} extensions shadow declared fields: #{collisions.join(', ')}"
393
+ end
394
+
395
+ payload.merge!(extras)
396
+ end
221
397
  end
222
398
  end
223
399
  end
@@ -44,10 +44,24 @@ module Selenium
44
44
  def presence(rules) = @presence = rules
45
45
  def fallback(path) = @fallback = path
46
46
 
47
- # A non-Hash payload is a bare scalar arm (e.g. input.Origin's "viewport") with
48
- # no object to dispatch on, so it is returned unchanged.
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") with no
57
+ # object to dispatch on, so it is returned unchanged — unless every arm is an object
58
+ # (object_only), where a non-Hash cannot match any variant and is a wire error.
49
59
  def from_json(json_payload)
50
- return json_payload unless json_payload.is_a?(::Hash)
60
+ unless json_payload.is_a?(::Hash)
61
+ return json_payload unless @object_only
62
+
63
+ raise Error::WebDriverError, "#{name} expected an object on the wire, got #{json_payload.inspect}"
64
+ end
51
65
 
52
66
  variant = select(json_payload)
53
67
  unless variant
@@ -75,8 +89,36 @@ module Selenium
75
89
  raise ::ArgumentError, "invalid combination for #{name}: #{invalid.join(', ')}"
76
90
  end
77
91
 
92
+ # Outbound mirror of from_json: is +value+ one this union accepts? Any variant is accepted,
93
+ # and a variant that is itself a union recurses (e.g. LocalValue's RemoteReference fallback).
94
+ # A non-object_only union (e.g. input.Origin) also admits one of its pinned bare-scalar
95
+ # literals; an object (a Hash or another union's record) that matched no variant does not.
96
+ def valid_outbound?(value)
97
+ return true if variant_refs.any? { |ref| variant_accepts?(ref, value) }
98
+
99
+ !@object_only && scalar_arm?(value)
100
+ end
101
+
78
102
  private
79
103
 
104
+ # A bare-scalar arm must be one of the literals the schema pinned for this union
105
+ # (scalar_values, e.g. input.Origin's "viewport" / "pointer"). The generator guarantees a
106
+ # non-object_only union declares them, so no runtime guard is needed here.
107
+ def scalar_arm?(value)
108
+ @scalar_values.include?(value)
109
+ end
110
+
111
+ # Every variant's class name: the discriminated table, the presence paths, and the fallback.
112
+ def variant_refs
113
+ @variant_refs ||= [*@variants&.values, *@presence&.keys, @fallback].compact
114
+ end
115
+
116
+ # A variant that is itself a union recurses; a record variant is matched by instance.
117
+ def variant_accepts?(ref, value)
118
+ klass = (@variant_classes ||= {})[ref] ||= Protocol.const_get(ref)
119
+ klass < Union ? klass.valid_outbound?(value) : value.is_a?(klass)
120
+ end
121
+
80
122
  # The discriminator value may legitimately be null (e.g. script.NullValue's
81
123
  # "null" tag), so it is matched by key presence.
82
124
  def select(json_payload)
@@ -21,7 +21,8 @@ module Selenium
21
21
  module WebDriver
22
22
  class BiDi
23
23
  # Wire round-trip runtime for the generated protocol layer: the value-type bases
24
- # (Record, Union), the omit sentinel (UNSET), and outbound enum validation.
24
+ # (Record, Union), the omit sentinel (UNSET), outbound enum validation, and the
25
+ # strict-inbound toggle.
25
26
  #
26
27
  # @api private
27
28
  module Serialization
@@ -33,6 +34,17 @@ module Selenium
33
34
  def UNSET.inspect = 'UNSET'
34
35
  UNSET.freeze
35
36
 
37
+ # Strict inbound mode. Off by default: a required field missing from a response is
38
+ # tolerated as omitted and warned, so a schema ahead of the browser does not block the
39
+ # caller. When SE_BIDI_STRICT is set to anything but 0/false, that same case escalates
40
+ # to an error for callers who want it.
41
+ #
42
+ # @api private
43
+ def self.strict?
44
+ value = ENV.fetch('SE_BIDI_STRICT', '').strip.downcase
45
+ !value.empty? && value != '0' && value != 'false'
46
+ end
47
+
36
48
  # Validates an outbound enum argument: +value+ is a symbol (or list of symbols) that
37
49
  # must be a key of the enum hash (+{symbol => wire_token}+), so a bad value fails
38
50
  # locally with a clear error instead of a round-trip. Outbound only; inbound wire