Module: BiDiGenerate Private

Defined in:
lib/selenium/webdriver/bidi/support/bidi_generate.rb,
lib/selenium/webdriver/bidi/support/check_generated.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

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

Defined Under Namespace

Classes: Command, Enum, Event, FieldIR, Module, Param, Schema, TypeClass, UnionClass, VariantIR

Constant Summary collapse

BIDI_DOC_URL =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Companion to the generated @api private tags: the page explaining why the BiDi implementation layer is internal and what higher-level API to use instead (see #17628).

'https://www.selenium.dev/documentation/warnings/bidi-implementation/'
LINE_LIMIT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

RuboCop's Layout/LineLength max; emitted Serialization::Record.define calls wrap to stay within it.

120
RUBY_RESERVED =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Ruby keywords that cannot be used as method names unquoted.

%w[begin end rescue ensure raise return yield if unless while until for do
case when then class module def].freeze
RESERVED_FIELD_NAMES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Object/Data methods a Data member name would shadow (breaking value semantics or reflection), e.g. a "method" field overriding Object#method.

(RUBY_RESERVED + %w[method hash class send dup clone freeze inspect
to_h to_s members with deconstruct deconstruct_keys
object_id tap itself then display
extensible extensions]).freeze
PARAMS_CLASS_KINDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Param kinds the named args can construct a Parameters object for (record fields, or a union dispatched to one of its variants); anything else forwards a raw hash.

%w[record union].freeze

Class Method Summary collapse

Class Method Details

.build_command(schema, cmd) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
# File 'lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 850

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

.build_event(schema, event) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



871
872
873
874
875
# File 'lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 871

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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



836
837
838
839
840
841
842
843
844
845
846
847
848
# File 'lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 836

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

.call(schema_path, output_dir) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



897
898
899
900
901
902
903
904
# File 'lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 897

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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



50
51
52
53
54
55
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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.



908
909
910
911
912
913
914
915
916
917
918
# File 'lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 908

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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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.



128
129
130
131
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



138
139
140
141
142
143
144
# File '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

.nest_synthetic(types) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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.



881
882
883
884
885
886
887
888
889
890
891
# File 'lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 881

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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



119
120
121
122
123
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



893
894
895
# File 'lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 893

def self.render(mod, template_path)
  ERB.new(File.read(template_path), trim_mode: '-').result(binding)
end

.ruby_literal(value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



75
76
77
78
79
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



106
107
108
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Append underscore to avoid clashing with Ruby reserved keywords.



93
94
95
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

$(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.



48
49
50
51
52
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



112
113
114
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



922
923
924
# File 'lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 922

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

.snake_to_class_name(snake) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



57
58
59
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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.



64
65
66
# File '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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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



69
70
71
72
# File '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

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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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.



84
85
86
87
88
89
90
# File '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