Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rb/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Metrics/ModuleLength:
Max: 110
Exclude:
- 'lib/selenium/webdriver/common/platform.rb'
- 'lib/selenium/webdriver/bidi/serialization/record.rb'
- 'lib/selenium/webdriver/bidi/support/bidi_generate.rb'
- 'spec/**/*'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ class ClipRectangle < Serialization::Union
# @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
# @see https://w3c.github.io/webdriver-bidi/#cddl-type-browsingcontextsetbypasscspparameters
SetBypassCSPParameters = Serialization::Record.define(
bypass: {wire_key: 'bypass', nullable: true},
bypass: {wire_key: 'bypass', nullable: true, const: true},
contexts: {wire_key: 'contexts', required: false, list: true},
user_contexts: {wire_key: 'userContexts', required: false, list: true}
)
Expand Down
2 changes: 1 addition & 1 deletion rb/lib/selenium/webdriver/bidi/protocol/emulation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class SetGeolocationOverrideParameters < Serialization::Union
# @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
# @see https://w3c.github.io/webdriver-bidi/#cddl-type-emulationsetscriptingenabledparameters
SetScriptingEnabledParameters = Serialization::Record.define(
enabled: {wire_key: 'enabled', nullable: true},
enabled: {wire_key: 'enabled', nullable: true, const: false},
contexts: {wire_key: 'contexts', required: false, list: true},
user_contexts: {wire_key: 'userContexts', required: false, list: true}
)
Expand Down
22 changes: 17 additions & 5 deletions rb/lib/selenium/webdriver/bidi/serialization/record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ module Serialization
# @api private
class Record < ::Data
# Named Field, not Member, to avoid colliding with +::Data#members+.
Field = ::Data.define(:name, :wire_key, :nullable, :ref, :list, :fixed, :enum, :required, :primitive, :scalar)
Field = ::Data.define(:name, :wire_key, :nullable, :ref, :list, :fixed, :enum, :required, :primitive,
:scalar, :const)

def self.define(**spec)
extensible = spec.delete(:extensible) || false
Expand Down Expand Up @@ -60,7 +61,7 @@ def self.field(name, meta)
nullable: meta[:nullable] || false, ref: meta[:ref],
list: meta[:list] || false, fixed: meta.fetch(:fixed, UNSET), enum: meta[:enum],
required: meta.fetch(:required, true), primitive: meta[:primitive],
scalar: meta[:scalar])
scalar: meta[:scalar], const: meta.fetch(:const, UNSET))
end
private_class_method :field

Expand Down Expand Up @@ -97,21 +98,32 @@ def from_json(json_payload)

# Checks each field's value: a required field cannot be omitted (UNSET), a non-nullable
# field cannot be nil (nil is neither a value nor the UNSET omit-sentinel, so it would be
# silently dropped on the wire), and an enum field must be in its allowed set. The enum
# constant is resolved lazily so a cross-domain enum need not be loaded first. Outbound
# only (from +new+); inbound presence/enum are checked separately in +wire_value+/+read+.
# silently dropped on the wire), a nullable-const field must carry its literal (not some
# other value), and an enum field must be in its allowed set. The enum constant is resolved
# lazily so a cross-domain enum need not be loaded first. Outbound only (from +new+);
# inbound presence/enum are checked separately in +wire_value+/+read+.
def validate_values(attributes)
fields.each do |f|
value = attributes[f.name]
raise ::ArgumentError, "#{name}##{f.name} is required" if UNSET.equal?(value) && f.required
raise ::ArgumentError, "#{name}##{f.name} cannot be nil" if value.nil? && !f.nullable
next if value.nil? || UNSET.equal?(value)

validate_const(f, value)
check_outbound_shape(f, value)
Serialization.validate!("#{name}##{f.name}", value, Protocol.const_get(f.enum)) if f.enum
end
end

# A nullable constant (`literal / null`) is caller-settable but its only non-null value is
# the literal, so a value that is neither the literal nor nil (nil is handled above) is a
# local error rather than a wire round-trip. A non-const field carries UNSET here and passes.
def validate_const(field, value)
return if UNSET.equal?(field.const) || value == field.const

raise ::ArgumentError, "#{name}##{field.name} must be #{field.const.inspect}, got #{value.inspect}"
end

# Outbound mirror of check_shape: a list-typed arg must be an array, a scalar-shaped one
# (enum or ref, not a list) must not β€” a local ArgumentError, not a wire round-trip.
def check_outbound_shape(field, value)
Expand Down
47 changes: 36 additions & 11 deletions rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,26 +236,35 @@ def type_entry = "'#{wire_name}' => #{payload_ref || 'nil'}"
# ref is the Protocol-relative class path for a nested structured field (nil
# for a scalar/opaque field); list wraps it in an array. wire_key is the exact
# JSON payload key (the schema's `wire` name, baked verbatim).
FieldIR = Struct.new(:ruby_name, :wire_key, :required, :nullable, :ref, :list, :enum, :primitive, :scalar, :rbs,
keyword_init: true) do
FieldIR = Struct.new(:ruby_name, :wire_key, :required, :nullable, :ref, :list, :enum, :primitive, :scalar, :const,
:rbs, keyword_init: true) do
# A `Serialization::Record.define` spec entry: `name: 'jsonKey'` shorthand, or
# `name: {wire_key:, …}` when the field carries JSON facts beyond its name.
# enum carries the allowed-values constant path, validated at construction.
def spec_entry(indent = 0)
meta = []
meta << 'required: false' unless required
meta << 'nullable: true' if nullable
meta << "ref: '#{ref}'" if ref
meta << 'list: true' if list
meta << "scalar: #{scalar_literal}" if scalar
meta << "enum: '#{enum}'" if enum
meta << "primitive: '#{primitive}'" if primitive
meta = value_facts
return "#{ruby_name}: '#{wire_key}'" if meta.empty?

meta.unshift("wire_key: '#{wire_key}'")
BiDiGenerate.wrap_call("#{ruby_name}: ", meta, indent, open: '{', close: '}')
end

# The JSON facts beyond the field's name, in the order Record.define reads them. A
# nullable const (`literal / null`) carries `const:` so the runtime rejects a value that
# is neither the literal nor null; `const.nil?` means the field has no const at all.
def value_facts
facts = []
facts << 'required: false' unless required
facts << 'nullable: true' if nullable
facts << "const: #{BiDiGenerate.ruby_literal(const)}" unless const.nil?
facts << "ref: '#{ref}'" if ref
facts << 'list: true' if list
facts << "scalar: #{scalar_literal}" if scalar
facts << "enum: '#{enum}'" if enum
facts << "primitive: '#{primitive}'" if primitive
facts
end

# The `scalar` primitive(s) a bare non-object wire value must match at a scalar-tolerant
# union position: a single primitive string, or an array when the union's scalar arms differ.
def scalar_literal
Expand Down Expand Up @@ -655,7 +664,23 @@ def field_ir(field)
required: field['required'], nullable: resolved[:nullable],
ref: resolved[:ref], list: resolved[:list], enum: enum_const(field['type']),
primitive: leaf_primitive(field['type']), scalar: resolved[:scalar],
rbs: resolved[:rbs])
const: leaf_const(field['type']), rbs: resolved[:rbs])
end

# The literal value of a const field, following alias chains, so the runtime can reject a
# value that is neither the literal nor null (a `literal / null` param such as
# emulation.setScriptingEnabled's `enabled`). Nil for any non-const node β€” const literals are
# never nil, so nil unambiguously means "no const" (a null value is carried by `nullable`).
def leaf_const(node, seen = {})
return node['const'] if node.key?('const')
return nil unless node.key?('ref')

name = node['ref']
type = @types[name]
return nil if seen[name] || type.nil? || type['kind'] != 'alias'

seen[name] = true
leaf_const(type['type'], seen)
end

# The runtime-checkable scalar primitive of a field, following alias chains so a
Expand Down
2 changes: 2 additions & 0 deletions rb/sig/lib/selenium/webdriver/bidi/serialization.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ module Selenium

def validate_values: (Hash[Symbol, untyped] attributes) -> void

def validate_const: (untyped field, untyped value) -> void

def check_outbound_shape: (untyped field, untyped value) -> void

def fixed?: (untyped field) -> bool
Expand Down
7 changes: 7 additions & 0 deletions rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ def valid_cookie_attrs
expect(BrowsingContext::SetBypassCSPParameters.new(bypass: true).as_json).to eq('bypass' => true)
expect(BrowsingContext::SetBypassCSPParameters.new(bypass: nil).as_json).to eq('bypass' => nil)
end

it 'rejects a value that is neither the literal nor null, before it reaches the wire' do
expect { BrowsingContext::SetBypassCSPParameters.new(bypass: false) }
.to raise_error(ArgumentError, /bypass must be true/)
expect { Emulation::SetScriptingEnabledParameters.new(enabled: true) }
.to raise_error(ArgumentError, /enabled must be false/)
end
end

describe 'extensible records' do
Expand Down
Loading