123456789_123456789_123456789_123456789_123456789_

Module: BiDiGenerate Private

Overview

Generates Ruby WebDriver BiDi protocol modules from the shared, binding-neutral BiDi schema produced by the JavaScript generator (see PR #17700):

//javascript/selenium-webdriver:create-bidi-src_schema -> bidi-schema.json

The schema is already normalized (inline enums hoisted, unions canonicalized, group composition flattened, wire names and nullability preserved verbatim), so this generator is a straight projection into Ruby with no CDDL interpretation.

Invoked via bazel run //rb/lib/selenium/webdriver:bidi-generate. Bazel passes the schema path (resolved through runfiles) plus the workspace-relative output directory as ARGV, and supplies the shared generated-note text as a runfile, so this is not runnable directly from a source checkout.

Constant Summary

Class Method Summary

Class Method Details

.accessor?(type, wrappers, plainly_reached) ⇒ Boolean

A type earns a send-side accessor when it is outbound and not a command param/result wrapper (a command method already builds those). A nested-away synthetic reached only as a union arm is excluded — it is built through its union (a variant factory or the command's flattened dispatch), never standalone. A top-level union variant record keeps its accessor (the plan constructs it directly, e.g. extension_path), as does a synthetic reached by a plain field ref (browsingContext.AccessibilityLocator's value).

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1266

def self.accessor?(type, wrappers, plainly_reached)
  return false unless type.outbound
  return false if wrappers.include?(type.schema_name)

  nested_synthetic = !type.union? && type.synthetic
  !nested_synthetic || plainly_reached.include?(type.schema_name)
end

.bidi_only_classes(codes)

Class names among codes the classic Error module does not already define — the BiDi-only codes bidi/error.rb registers and whose RBS this file must declare. Shared codes already have RBS in common/error.rbs, so re-declaring them would duplicate the classic signatures. Only the RBS needs this split; the emitted map (error_code.rb) stays the full self-contained set.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1385

def self.bidi_only_classes(codes)
  require_relative '../../common/error'
  codes.filter_map { |_wire, name| name unless ::Selenium::WebDriver::Error.const_defined?(name, false) }
end

.build_accessors(schema, domain, types)

Outbound-scoped domain accessors: for every emitted type a caller constructs to send, a prefix-free constructor on the Domain subclass. Built from the pre-nesting type list so a nested synthetic (referenced by a Ruby-relative Owner::Label path) is reachable.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1250

def self.build_accessors(schema, domain, types)
  wrappers = schema.command_wrapper_refs(domain)
  plainly_reached = schema.plainly_reached_types
  types.select { |t| accessor?(t, wrappers, plainly_reached) }.map do |t|
    Accessor.new(method_name: safe_method_name(camel_to_snake(type_class_name(t.schema_name))),
                 type_name: schema.domain_relative_path(t.schema_name), union: t.union?,
                 rbs_args: t.union? ? nil : t.rbs_new_args)
  end
end

.build_command(schema, cmd)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1297

def self.build_command(schema, cmd)
  params = schema.params_for(cmd['params'])
  # A param that can't flatten to a typed object (alias or non-record union) would be
  # silently dropped, so fail generation and handle that shape deliberately if it appears.
  if cmd['params'] && params.nil?
    raise "command #{cmd['method']} has params that cannot be expressed as a typed object"
  end

  params_ref = cmd['params'] && cmd['params']['ref']
  params_kind = schema.type_kind(params_ref)
  params_class = type_class_name(params_ref) if !params.empty? && PARAMS_CLASS_KINDS.include?(params_kind)
  Command.new(
    wire_name: cmd['method'],
    method_name: safe_method_name(camel_to_snake(cmd['name'])),
    params: params,
    result_ref: cmd['result'] && schema.structured_ref(cmd['result']['ref']),
    params_class: params_class,
    union_params: params_kind == 'union',
    spec_href: cmd['specHref']
  )
end

.build_event(schema, event)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1319

def self.build_event(schema, event)
  params = event['params']
  payload_ref = params && params['ref'] && schema.structured_ref(params['ref'])
  Event.new(wire_name: event['method'], event_name: camel_to_snake(event['name']), payload_ref: payload_ref)
end

.build_ir(schema)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1203

def self.build_ir(schema)
  schema.domains.map do |domain|
    types = schema.types_for(domain)
    thread_variant_arg_sigs(schema, types)
    vendor_modules = schema.vendor_modules_for(domain)
    mod = Module.new(
      name: domain,
      ruby_class: snake_to_class_name(camel_to_snake(domain)),
      filename: camel_to_snake(domain),
      commands: schema.commands_for(domain).map { |cmd| build_command(schema, cmd) },
      events: schema.events_for(domain).map { |ev| build_event(schema, ev) },
      enums: schema.enums_for(domain),
      accessors: build_accessors(schema, domain, types) + vendor_accessors(vendor_modules),
      types: nest_synthetic(types),
      vendor_modules: vendor_modules,
      spec_href: schema.domain_href(domain)
    )
    check_accessor_collisions!(mod)
    mod
  end
end

.call(schema_path, output_dir)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1347

def self.call(schema_path, output_dir)
  raw = load_json(schema_path)
  schema = Schema.new(raw)
  modules = build_ir(schema)

  emit(modules, output_dir, 'module.rb.erb', 'rb')
  emit(modules, sig_dir(output_dir), 'module.rbs.erb', 'rbs')
  emit_error_module(schema, output_dir)
end

.camel_to_snake(str)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 51

def self.camel_to_snake(str)
  str
    .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
    .gsub(/([a-z\d])([A-Z])/, '\1_\2')
    .downcase
end

.check!(schema_rootpath)

Verifies the checked-in protocol .rb match what the generator would produce from the current schema — catching a hand-edit or a forgotten regeneration. Re-renders each module in memory (no file writes) and compares. The .rbs are covered by Steep.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/check_generated.rb', line 27

def self.check!(schema_rootpath)
  schema = Schema.new(JSON.parse(File.read(schema_path(schema_rootpath))))
  protocol_dir = File.expand_path('../protocol', __dir__)
  template = File.join(__dir__, 'templates', 'module.rb.erb')

  stale = build_ir(schema).filter_map do |mod|
    path = File.join(protocol_dir, "#{mod.filename}.rb")
    "#{mod.filename}.rb" unless File.exist?(path) && File.read(path) == render(mod, template)
  end
  stale << 'error_code.rb' unless error_module_current?(schema, protocol_dir)
  return if stale.empty?

  warn "Generated BiDi protocol code is stale or hand-edited: #{stale.sort.join(', ')}"
  warn 'Regenerate with: bazel run //rb/lib/selenium/webdriver:bidi-generate'
  exit 1
end

.check_accessor_collisions!(mod)

Fail generation if an accessor name would collide with a command method, an inherited method, or another accessor — turning a future shadow into a build error rather than a silently overridden method.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1282

def self.check_accessor_collisions!(mod)
  commands = mod.commands.to_set(&:method_name)
  seen = {}
  mod.accessors.each do |accessor|
    name = accessor.method_name
    clash = if commands.include?(name) then 'a command method'
            elsif INHERITED_INSTANCE_METHODS.include?(name) then 'an inherited method'
            elsif seen[name] then "the accessor for #{seen[name]}"
            end
    raise "accessor #{mod.ruby_class}##{name} collides with #{clash}" if clash

    seen[name] = accessor.type_name
  end
end

.emit(modules, output_dir, template, extension)

Renders every module through one template and writes the result into target, one file per module. Used for both the Ruby source and its RBS signatures.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1392

def self.emit(modules, output_dir, template, extension)
  target = File.join(workspace_root, output_dir)
  FileUtils.mkdir_p(target)

  tmpl = File.join(File.dirname(__FILE__), 'templates', template)
  modules.each do |mod|
    path = File.join(target, "#{mod.filename}.#{extension}")
    File.write(path, render(mod, tmpl))
    warn "bidi-generate: wrote #{path}"
  end
end

.emit_error_module(schema, output_dir)

Writes protocol/error_code.rb (+ its .rbs), the Protocol::ErrorCode map, into the same protocol dir as the generated domain files.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1374

def self.emit_error_module(schema, output_dir)
  codes = error_code_map(schema)
  mod = ErrorModule.new(filename: 'error_code', codes: codes, new_classes: bidi_only_classes(codes))
  emit([mod], output_dir, 'error_code.rb.erb', 'rb')
  emit([mod], sig_dir(output_dir), 'error_code.rbs.erb', 'rbs')
end

.enum_const_path(type_name)

Domain-qualified path to an enum's frozen hash constant ("browsingContext.ReadinessState" → "BrowsingContext::READINESS_STATE"), so a generated command method can reference it for an outbound membership check.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 132

def self.enum_const_path(type_name)
  domain, local = type_name.split('.', 2)
  "#{snake_to_class_name(camel_to_snake(domain))}::#{screaming_snake(local)}"
end

.enum_key(value)

snake_case hash key for an enum value (only a label for the wire value it maps to). Preserves camelCase word boundaries (beforeRequestSent → before_request_sent), maps a leading minus to "neg" (-0 → neg0, -Infinity → neg_infinity; no underscore before a digit, so the key stays normalcase), and collapses other punctuation (dedicated-worker → dedicated_worker).

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 142

def self.enum_key(value)
  camel_to_snake(value.to_s)
    .sub(/\A-(?=\d)/, 'neg')
    .sub(/\A-/, 'neg_')
    .gsub(/[^a-z0-9]+/, '_')
    .gsub(/\A_|_\z/, '')
end

.error_class_name(code)

WebDriver error-code string -> exception class name, matching Error.for_error's convention ("no such node" -> NoSuchNodeError). The Error suffix is normalized (not doubled) for a code already ending in "error" ("unknown error" -> UnknownError).

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1368

def self.error_class_name(code)
  "#{code.split.map(&:capitalize).join.sub(/Error$/, '')}Error"
end

.error_code_map(schema)

The ErrorCode wire values mapped to their Ruby exception class names (schema order), e.g. "no such node" => "NoSuchNodeError". This is the schema->Ruby translation: the generated file carries the Ruby names, and a hand-written pass turns them into WebDriverError subclasses under the shared Error namespace. Self-contained — no reference to the classic error module.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1361

def self.error_code_map(schema)
  schema.error_codes.map { |code| [code, error_class_name(code)] }
end

.error_module_current?(schema, protocol_dir) ⇒ Boolean

Whether the checked-in protocol/error_code.rb matches what the generator would render now.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/check_generated.rb', line 45

def self.error_module_current?(schema, protocol_dir)
  mod = ErrorModule.new(filename: 'error_code', codes: error_code_map(schema))
  path = File.join(protocol_dir, 'error_code.rb')
  File.exist?(path) && File.read(path) == render(mod, File.join(__dir__, 'templates', 'error_code.rb.erb'))
end

.load_json(path) (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1410

private_class_method def self.load_json(path)
  resolved = File.exist?(path) ? path : File.join(Dir.pwd, path)
  JSON.parse(File.read(resolved))
end

.nest_synthetic(types)

The projector tags lifted-out types with owner, label. Emit each synthetic record inside its owner's class body under its bare label, so Owner_Label becomes the nested Owner::Label (refs resolve there via ruby_path). Synthetic enums stay domain-level. Raises on a missing owner.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1329

def self.nest_synthetic(types)
  index = types.to_h { |t| [t.schema_name, t] }
  children = types.select { |t| !t.union? && t.synthetic }
  children.each do |child|
    owner = index[child.owner] ||
            raise("synthetic type #{child.schema_name} has no emitted owner #{child.owner}")
    owner.nested = (owner.nested || []) << child
    child.ruby_name = child.label
  end
  types - children
end

.rbs_nilable(type)

Makes an RBS type admit nil, idempotently (an already-nilable or opaque type is left as-is). Applied to a field whose schema type is nullable, so its value type allows nil; keyword-optionality is expressed separately by the ? prefix (see rbs_part / rbs_arg).

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 123

def self.rbs_nilable(type)
  return type if type == 'untyped' || type == 'nil' || type.end_with?('?')

  "#{type}?"
end

.render(mod, template_path)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1341

def self.render(mod, template_path)
  generated_note = GeneratedNote.render('#', 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb',
                                        'bazel run //rb/lib/selenium/webdriver:bidi-generate')
  ERB.new(File.read(template_path), trim_mode: '-').result(binding)
end

.ruby_literal(value)

Source literal for a discriminator/const value (string, boolean, or number).

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 76

def self.ruby_literal(value)
  return 'nil' if value.nil?

  value.is_a?(String) ? "'#{value}'" : value.to_s
end

.safe_field_name(name)

Append underscore to a field name that would shadow a core method; the wire name is unaffected, only the Ruby reader is renamed.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 107

def self.safe_field_name(name)
  # A vendor-prefixed wire name carries a colon (moz:allowPrivateBrowsing); swap it
  # for an underscore so the Ruby reader is a legal identifier. The wire key is kept.
  name = name.tr(':', '_')
  RESERVED_FIELD_NAMES.include?(name) ? "#{name}_" : name
end

.safe_method_name(name)

Append underscore to avoid clashing with Ruby reserved keywords.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 94

def self.safe_method_name(name)
  RUBY_RESERVED.include?(name) ? "#{name}_" : name
end

.schema_path(rootpath)

$(rootpath) is relative to the runfiles root; dir anchors us there so it resolves the same way locally and on RBE (an execpath would not). This file lives at rb/lib/selenium/webdriver/bidi/support, so expand six levels up to the root — File.expand_path is separator-agnostic, unlike stripping a "/"-spelled suffix (which would miss on Windows). The cwd-relative rootpath fallback matches how spec_support's rlocation resolves.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/check_generated.rb', line 56

def self.schema_path(rootpath)
  runfiles_root = File.expand_path('../../../../../..', __dir__)
  [File.join(runfiles_root, rootpath), rootpath].find { |p| File.exist?(p) } ||
    raise("BiDi schema not found (looked for #{rootpath})")
end

.screaming_snake(camel)

SCREAMING_SNAKE constant name for an enum, matching the EVENTS map style (ReadinessState → READINESS_STATE).

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 116

def self.screaming_snake(camel)
  camel_to_snake(camel).upcase
end

.sig_dir(output_dir)

The RBS signatures mirror the source tree under sig/ (the repo's convention), e.g. rb/lib/.../protocol -> rb/sig/lib/.../protocol.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1406

def self.sig_dir(output_dir)
  output_dir.sub(%r{(\A|/)lib/}, '\1sig/lib/')
end

.snake_to_class_name(snake)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 58

def self.snake_to_class_name(snake)
  snake.split('_').map(&:capitalize).join
end

.thread_variant_arg_sigs(schema, types)

Give each union its variants' typed new signatures, keyed by factory method name, so rbs_variant_factories can emit a checked signature instead of a splat. Keyed by ruby path (the form a variant ref carries); a cross-module variant not in this list falls back to **untyped.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1238

def self.thread_variant_arg_sigs(schema, types)
  record_sigs = types.reject(&:union?).to_h { |t| [schema.ruby_path_for(t.schema_name), t.rbs_new_args] }
  types.select(&:union?).each do |union|
    union.variant_arg_sigs = union.value_variants.to_h do |variant|
      [BiDiGenerate.enum_key(variant.value), record_sigs[variant.ref]]
    end
  end
end

.type_class_name(type_name)

Local constant for a domain-scoped type: "script.LocalValue" -> "LocalValue". The first letter is capitalized so a lower-cased spec name (e.g. "permissions.setPermission") still yields a valid Ruby constant.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 65

def self.type_class_name(type_name)
  type_name.split('.', 2).last.sub(/\A[a-z]/, &:upcase)
end

.type_ruby_path(type_name)

Protocol-relative class path: "script.LocalValue" -> "Script::LocalValue".

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 70

def self.type_ruby_path(type_name)
  domain = type_name.split('.', 2).first
  "#{snake_to_class_name(camel_to_snake(domain))}::#{type_class_name(type_name)}"
end

.vendor_accessors(vendor_modules)

An accessor per vendor variant, returning a sibling vendor domain over the same connection (web_extension.moz -> Moz.new(connection)). Named after the vendor namespace.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1227

def self.vendor_accessors(vendor_modules)
  vendor_modules.map do |vendor_module|
    Accessor.new(method_name: safe_method_name(vendor_module.namespace), type_name: vendor_module.name,
                 union: false, vendor: true)
  end
end

.workspace_root (private)

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 1415

private_class_method def self.workspace_root
  ENV['BUILD_WORKSPACE_DIRECTORY'] || Dir.pwd
end

.wrap_call(prefix, args, indent, open: '(', close: ')')

Renders prefix(args) on one line, or one argument per line when it would exceed LINE_LIMIT at the given indent. open/close default to parentheses (pass {} for a hash literal) so emitted calls and literals stay within RuboCop's length limit.

[ GitHub ]

  
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 85

def self.wrap_call(prefix, args, indent, open: '(', close: ')')
  one_line = "#{prefix}#{open}#{args.join(', ')}#{close}"
  return one_line if args.empty? || indent + one_line.length <= LINE_LIMIT

  pad = ' ' * (indent + 2)
  "#{prefix}#{open}\n#{pad}#{args.join(",\n#{pad}")}\n#{' ' * indent}#{close}"
end