Module: BiDiGenerate Private
| Relationships & Source Files | |
| Namespace Children | |
|
Classes:
| |
| Defined in: | rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb, rb/lib/selenium/webdriver/bidi/support/check_generated.rb |
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
-
BIDI_DOC_URL =
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 41
Companion to the generated
@api privatetags: 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 =
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 44
RuboCop's Layout/LineLength max; emitted
Serialization::Record.definecalls wrap to stay within it.120 -
PARAMS_CLASS_KINDS =
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 881
::BiDiGenerate::Paramkinds 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
-
RESERVED_FIELD_NAMES =
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 99
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
-
RUBY_RESERVED =
# File 'rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb', line 47
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
Class Method Summary
- .build_command(schema, cmd) Internal use only
- .build_event(schema, event) Internal use only
- .build_ir(schema) Internal use only
- .call(schema_path, output_dir) Internal use only
- .camel_to_snake(str) Internal use only
-
.check!(schema_rootpath)
Internal use only
Verifies the checked-in protocol
.rbmatch what the generator would produce from the current schema — catching a hand-edit or a forgotten regeneration. -
.emit(modules, output_dir, template, extension)
Internal use only
Renders every module through one template and writes the result into target, one file per module.
-
.enum_const_path(type_name)
Internal use only
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.
-
.enum_key(value)
Internal use only
snake_case hash key for an enum value (only a label for the wire value it maps to).
-
.nest_synthetic(types)
Internal use only
The projector tags lifted-out types with
owner, label. -
.rbs_nilable(type)
Internal use only
Makes an RBS type admit nil, idempotently (an already-nilable or opaque type is left as-is).
- .render(mod, template_path) Internal use only
-
.ruby_literal(value)
Internal use only
Source literal for a discriminator/const value (string, boolean, or number).
-
.safe_field_name(name)
Internal use only
Append underscore to a field name that would shadow a core method; the wire name is unaffected, only the Ruby reader is renamed.
-
.safe_method_name(name)
Internal use only
Append underscore to avoid clashing with Ruby reserved keywords.
-
.schema_path(rootpath)
Internal use only
$(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).
-
.screaming_snake(camel)
Internal use only
SCREAMING_SNAKE constant name for an enum, matching the EVENTS map style (ReadinessState → READINESS_STATE).
-
.sig_dir(output_dir)
Internal use only
The RBS signatures mirror the source tree under sig/ (the repo's convention), e.g.
- .snake_to_class_name(snake) Internal use only
-
.type_class_name(type_name)
Internal use only
Local constant for a domain-scoped type: "script.LocalValue" -> "LocalValue".
-
.type_ruby_path(type_name)
Internal use only
Protocol-relative class path: "script.LocalValue" -> "Script::LocalValue".
-
.wrap_call(prefix, args, indent, open: '(', close: ')')
Internal use only
Renders
prefix(args)on one line, or one argument per line when it would exceed LINE_LIMIT at the given indent. - .load_json(path) private Internal use only
- .workspace_root private Internal use only
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.
# 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.('../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.
# 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.
# 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).
# 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.
# 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).
# 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).
# 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.
# 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.
# 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.
# File 'rb/lib/selenium/webdriver/bidi/support/check_generated.rb', line 48
def self.schema_path(rootpath) runfiles_root = File.('../../../../../..', __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).
# 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.
# 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.
# 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".
# 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.
# 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