Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
590 changes: 571 additions & 19 deletions common/bidi/schema.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions common/webref_cddl.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
# Pinned to a single w3c/webref "main" commit (kept on main so pin-to-pin diffs
# stay auditable) and regenerated by the //scripts:update_cddl Bazel target:
# https://github.com/w3c/webref/tree/main/ed/cddl
_COMMIT = "f6229ab7d17cf855385c98919a671c5a6ca37341"
_COMMIT = "5e914258243bbca0ccbfe184bdc62eb82d53c6b7"
_BASE_URL = "https://raw.githubusercontent.com/w3c/webref/{commit}/ed/cddl".format(commit = _COMMIT)

# The webref "definitions index" for each spec (same commit, so it stays in lockstep
Expand All @@ -21,8 +21,8 @@ _DFNS_BASE_URL = "https://raw.githubusercontent.com/w3c/webref/{commit}/ed/dfns"
# step runs extract_bidi_anchors.mjs over it to build the prose-anchor index. This is a
# separate repo/branch from the webref pin above, so //scripts:update_cddl repins it to
# gh-pages' own tip; both track the same editor's draft, within a day of each other.
_BIDI_SPEC_HTML_COMMIT = "afa9413adb1f11da188d0cb4af19ce84d596ec19"
_BIDI_SPEC_HTML_SHA256 = "9c731584b1eb856b343cc0d05f450f80cbb2e802ad4af6b92a5224accc3b20e0"
_BIDI_SPEC_HTML_COMMIT = "975a118c247b6f0e0b64f8093b5f01238423bab1"
_BIDI_SPEC_HTML_SHA256 = "ddfc94f2d7692a3da189c997cb3e88388044b661d2459e68cf7a977a11807446"
_BIDI_SPEC_HTML_URL = "https://raw.githubusercontent.com/w3c/webdriver-bidi/{commit}/index.html".format(
commit = _BIDI_SPEC_HTML_COMMIT,
)
Expand All @@ -41,7 +41,7 @@ _CDDL_FILES = [
("prefetch_all_cddl", "prefetch-all.cddl", "51409b998176a81f681252f8ee16bea5a54a3be9d1cfee9f13ca34efd1feb5ea"),
("ua_client_hints_all_cddl", "ua-client-hints-all.cddl", "6bb41f05d09c755305226b7350970e54f6404698510ea2e1e7a931eaa2647aeb"),
("web_bluetooth_all_cddl", "web-bluetooth-all.cddl", "bc687d19c1e92cf4f0a37bbf7f9aa21db956344b34d8cdda4319b7f4615b6d1d"),
("webdriver_bidi_all_cddl", "webdriver-bidi-all.cddl", "912e70e34c48e02ef871756b020b1d370189c88574f356021b7cb5b5cfd7ab98"),
("webdriver_bidi_all_cddl", "webdriver-bidi-all.cddl", "ddc307a435c508bdccdaef3c7869187f5f91fe48510162b099a4daabff2dbfc0"),
]

def webref_cddl():
Expand All @@ -59,7 +59,7 @@ def webref_cddl():
# and referenced as @<repo_name>//file:dfns.json. Only the specs merged into the BiDi
# schema (see the javascript/ and py/ BUILD merge lists) need one.
_DFNS_FILES = [
("webdriver_bidi_dfns", "webdriver-bidi.json", "f57dc9eb577551478b0c6449009dd6fa8d635dfa025ec8f62a1921709aa9ecee"),
("webdriver_bidi_dfns", "webdriver-bidi.json", "80823455873e25af598e6d5a6b616dc84f96b2649d5623b962092df596fcd090"),
("permissions_dfns", "permissions.json", "bc5d2907c61e2548c6fc4a42a32c86396b56a68033c62299c4d563db510c105f"),
("prefetch_dfns", "prefetch.json", "b647ef493f8f09a6266c2269c344b16b28344bd14b2b987d660df01ce5833433"),
("ua_client_hints_dfns", "ua-client-hints.json", "56a31bf6fd2ba03158500498d14742f8edd7a2d560580b4aa7f4a703977e40cf"),
Expand Down
43 changes: 37 additions & 6 deletions javascript/selenium-webdriver/project_bidi_schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* straight mapping into a small vocabulary:
*
* type node: { kind: 'record', fields: [field], map?, extensible?, specHref? }
* | { kind: 'enum', values: [string], specHref? }
* | { kind: 'enum', values: [scalar], primitive?, specHref? }
* | { kind: 'union', variants: [ref], selector, objectOnly?, specHref? }
* | { kind: 'alias', type, specHref? }
* selector: { by, variants: [{ value, ref }], default? } // discriminated
Expand Down Expand Up @@ -216,17 +216,32 @@ function projectEntry(e) {
return { primitive: PRIMITIVES[e.Type] ?? 'unknown' }
}

// A wire key that is already an identifier is kept verbatim, so `namespaceURI` stays
// wire-faithful; a quoted CDDL key that is not (`prefers-color-scheme`) is camelCased so
// every binding derives an identifier from `name` without re-solving punctuation itself.
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9]*$/

function fieldName(wire) {
if (IDENTIFIER.test(wire)) return wire
const [head, ...rest] = wire.split(/[^A-Za-z0-9]+/).filter(Boolean)
if (!head) return wire
return head + rest.map((part) => part[0].toUpperCase() + part.slice(1)).join('')
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}

function projectField(prop) {
// A vendor-prefixed key (moz:allowPrivateBrowsing) keeps its wire form: extractVendor
// routes it out of the shared types, and the vendor pipeline drops the namespace itself.
const vendor = prop['x-selenium-vendor']
const field = {
name: prop.Name,
name: vendor ? prop.Name : fieldName(prop.Name),
wire: prop.Name,
required: (prop.Occurrence?.n ?? 1) >= 1,
type: projectRef(prop.Type),
}
// Provenance stamped by a vendor overlay (generate_bidi.mjs). Carried on the field so
// extractVendor can route it out of the shared schema; stripped there before it ships.
if (prop['x-selenium-vendor']) {
field.vendor = prop['x-selenium-vendor']
if (vendor) {
field.vendor = vendor
field.via = prop['x-selenium-vendor-via']
}
return field
Expand All @@ -252,7 +267,14 @@ function unionMemberRefs(def) {
function projectType(def) {
if (def.Type === 'variable') {
const pt = def.PropertyType ?? []
if (pt.length && pt.every(isLiteral)) return { kind: 'enum', values: pt.map((e) => e.Value) }
if (pt.length && pt.every(isLiteral)) {
// Carry the literals' shared primitive so every binding reads the value type
// rather than re-deriving it from the JSON values (which arrive differently typed
// per language). Matches what enumNode does for an un-hoisted inline choice.
const values = pt.map((e) => e.Value)
const primitive = literalPrimitive(values)
return primitive ? { kind: 'enum', values, primitive } : { kind: 'enum', values }
}
// A union of refs is a union even when some arms are inline groups wrapping a
// ref (e.g. script.LocalValue's date/regexp arms): projectRef resolves those to
// refs, so promote the all-ref result to a first-class union (it gets a selector)
Expand Down Expand Up @@ -792,7 +814,16 @@ export function checkSchema(schema) {
for (const [name, node] of Object.entries(schema.types)) {
if (node.synthetic && !has(node.owner)) errors.push(`${name}: synthetic owner ${node.owner} does not resolve`)
if (node.kind === 'record') {
for (const f of node.fields) report(`${name}.${f.name}`, f.type)
const wireByName = new Map()
for (const f of node.fields) {
report(`${name}.${f.name}`, f.type)
// Two wire keys that camelCase to one name would collapse into a single
// attribute downstream, silently keeping whichever the binding wrote last.
const prior = wireByName.get(f.name)
if (prior !== undefined && prior !== f.wire)
errors.push(`${name}: wire keys ${prior} and ${f.wire} both project to field name ${f.name}`)
wireByName.set(f.name, f.wire)
}
if (node.map) report(`${name}.*`, node.map)
} else if (node.kind === 'union') {
for (const v of node.variants) if (!has(v)) errors.push(`${name}: unresolved variant ${v}`)
Expand Down
42 changes: 42 additions & 0 deletions javascript/selenium-webdriver/project_bidi_schema_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('projectSchema', () => {
assert.deepEqual(schema.types['network.SetCacheBehaviorParametersCacheBehavior'], {
kind: 'enum',
values: ['default', 'bypass'],
primitive: 'string',
synthetic: true,
owner: 'network.SetCacheBehaviorParameters',
label: 'CacheBehavior',
Expand All @@ -73,6 +74,27 @@ describe('projectSchema', () => {
})
})

it('tags a numeric enum with its value primitive, so a binding reads the type rather than deriving it', () => {
const ast = [group('x.T', [field('grid', [lit(0), lit(1)])])]
const [, def] = Object.entries(projectSchema(ast, {}).types).find(([, t]) => t.kind === 'enum')
assert.deepEqual(def.values, [0, 1])
assert.equal(def.primitive, 'integer')
})

it('camelCases a quoted wire key into an identifier, keeping the wire name exact', () => {
const ast = [group('x.T', [field('prefers-color-scheme', ['text'], { n: 0, m: 1 })])]
const [f] = projectSchema(ast, {}).types['x.T'].fields
assert.equal(f.name, 'prefersColorScheme')
assert.equal(f.wire, 'prefers-color-scheme')
})

it('leaves a wire key that is already an identifier verbatim (namespaceURI is not mangled)', () => {
const ast = [group('x.T', [field('namespaceURI', ['text'])])]
const [f] = projectSchema(ast, {}).types['x.T'].fields
assert.equal(f.name, 'namespaceURI')
assert.equal(f.wire, 'namespaceURI')
})

it('hoists an inline record so the field is a plain ref (no inline records), tagged with its origin', () => {
assert.deepEqual(schema.types['session.Caps'].fields[0].type, { ref: 'session.CapsExtra' })
const extra = schema.types['session.CapsExtra']
Expand Down Expand Up @@ -772,6 +794,26 @@ describe('checkSchema (referential integrity)', () => {
assert.deepEqual(checkSchema(schema), ['x.T.a: unresolved type x.Missing'])
})

it('flags two wire keys projecting to one field name', () => {
const schema = {
schemaVersion: 1,
commands: [],
events: [],
types: {
'x.T': {
kind: 'record',
fields: [
{ name: 'colorGamut', wire: 'colorGamut', required: false, type: { primitive: 'string' } },
{ name: 'colorGamut', wire: 'color-gamut', required: false, type: { primitive: 'string' } },
],
},
},
}
assert.deepEqual(checkSchema(schema), [
'x.T: wire keys colorGamut and color-gamut both project to field name colorGamut',
])
})

it('catches an unresolved ref inside an alias', () => {
const schema = {
schemaVersion: 1,
Expand Down
13 changes: 11 additions & 2 deletions py/generate_bidi_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@

_RESERVED_FIELDS = {"as_json", "from_json", "extensions"}

# The Enum mixin for a schema enum's declared value primitive; anything else stays str.
_ENUM_MIXINS = {"integer": "int", "number": "float"}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated


# --------------------------------------------------------------------------- #
# Naming helpers
Expand Down Expand Up @@ -188,6 +191,7 @@ class EnumIR:
class_name: str
schema_name: str
members: list[tuple[str, Any]]
primitive: str | None = None
spec_href: str | None = None


Expand Down Expand Up @@ -301,7 +305,11 @@ def enums_for(self, domain: str) -> list[EnumIR]:
members = [(enum_member(v), v) for v in type_["values"]]
out.append(
EnumIR(
class_name=type_class_name(name), schema_name=name, members=members, spec_href=type_.get("specHref")
class_name=type_class_name(name),
schema_name=name,
members=members,
primitive=type_.get("primitive"),
spec_href=type_.get("specHref"),
)
)
return out
Expand Down Expand Up @@ -837,7 +845,8 @@ def _spec_docstring(name: str, spec_href: str | None) -> list[str]:


def _emit_enum(e: EnumIR) -> str:
lines = [f"@register({lit(e.schema_name)})", f"class {e.class_name}(str, Enum):"]
mixin = _ENUM_MIXINS.get(e.primitive, "str")
lines = [f"@register({lit(e.schema_name)})", f"class {e.class_name}({mixin}, Enum):"]
Comment thread
titusfortner marked this conversation as resolved.
Outdated
lines += _spec_docstring(e.schema_name, e.spec_href)
for member, value in e.members:
lines.append(f" {member} = {lit(value)}")
Expand Down
Loading
Loading