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. Can also be run directly:

ruby bidi_generate.rb schema.json output/dir

Constant Summary

Class Method Summary

Class Method Details

.build_command(schema, cmd)

[ GitHub ]

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

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 920

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 883

def self.build_ir(schema)
  schema.domains.map do |domain|
    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),
      types: nest_synthetic(schema.types_for(domain)),
      spec_href: schema.domain_href(domain)
    )
  end
end

.call(schema_path, output_dir)

[ GitHub ]

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

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')
end

.camel_to_snake(str)

[ GitHub ]

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

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)
  modules = build_ir(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 = modules.reject do |mod|
    path = File.join(protocol_dir, "#{mod.filename}.rb")
    File.exist?(path) && File.read(path) == render(mod, template)
  end
  return if stale.empty?

  warn "Generated BiDi protocol code is stale or hand-edited: #{stale.map { |m| "#{m.filename}.rb" }.sort.join(', ')}"
  warn 'Regenerate with: bazel run //rb/lib/selenium/webdriver:bidi-generate'
  exit 1
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 957

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

.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 128

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 138

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

.load_json(path) (private)

[ GitHub ]

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

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 930

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 119

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 942

def self.render(mod, template_path)
  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 75

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 106

def self.safe_field_name(name)
  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 93

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 48

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 112

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 971

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 57

def self.snake_to_class_name(snake)
  snake.split('_').map(&:capitalize).join
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 64

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 69

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

.workspace_root (private)

[ GitHub ]

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

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 84

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