selenium-webdriver 4.46.0 → 4.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.md +30 -0
- package/README.md +13 -10
- package/bidi/domain.js +87 -0
- package/bidi/generated/common.js +3 -0
- package/bidi/generated/emulation.d.ts +29 -2
- package/bidi/generated/emulation.js +18 -0
- package/bidi/generated/webextension.d.ts +5 -4
- package/bidi/index.js +187 -15
- package/bidi/serialization/enum.js +35 -0
- package/bidi/serialization/record.js +345 -0
- package/bidi/serialization/registry.js +34 -0
- package/bidi/serialization/union.js +102 -0
- package/bin/linux/selenium-manager +0 -0
- package/bin/macos/selenium-manager +0 -0
- package/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +5940 -0
- package/bin/selenium-manager.cdx.json +11708 -0
- package/bin/windows/selenium-manager.exe +0 -0
- package/firefox.js +1 -0
- package/index.js +13 -0
- package/lib/atoms/find-elements.js +19 -1
- package/lib/atoms/get-attribute.js +19 -1
- package/lib/atoms/is-displayed.js +19 -1
- package/lib/bidi_connection.js +84 -0
- package/lib/logging.js +35 -0
- package/lib/proxy.js +3 -16
- package/lib/webdriver.js +56 -19
- package/package.json +11 -11
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The SFC licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
|
|
18
|
+
const { register, resolve } = require('./registry')
|
|
19
|
+
|
|
20
|
+
class ValidationError extends Error {}
|
|
21
|
+
|
|
22
|
+
// Validates a *present* value against a resolved type node, and returns the value to
|
|
23
|
+
// assign for it — usually the same value, but for a nested ref-to-record/union parsed
|
|
24
|
+
// inbound (fromWire()/build() results, not the raw wire object) and for list/map/inline
|
|
25
|
+
// record values (a fresh, deeply-frozen copy, so validity can't be corrupted after the
|
|
26
|
+
// fact by mutating an array or object a caller still holds a reference to).
|
|
27
|
+
// Outbound nested refs deliberately keep the caller's own value instead of the
|
|
28
|
+
// newly-constructed instance — the nested `new referenced.RecordClass(value)` /
|
|
29
|
+
// `referenced.build(value)` call below exists only to validate, matching the
|
|
30
|
+
// constructor's existing outbound behavior of trusting the caller's own object shape.
|
|
31
|
+
// `direction` only affects how a nested ref-to-record/union is itself validated/parsed.
|
|
32
|
+
function validateValue(typeNode, value, path, direction) {
|
|
33
|
+
if (value === null) {
|
|
34
|
+
// `primitive: 'null'` (project_bidi_schema.mjs's projectRef(), for a type whose
|
|
35
|
+
// every alternative was null) means null itself is the valid value — accept it
|
|
36
|
+
// even though `nullable` (a *different* value also being permitted alongside a
|
|
37
|
+
// non-null base type) wasn't separately set.
|
|
38
|
+
if (typeNode.nullable || typeNode.primitive === 'null') return null
|
|
39
|
+
throw new ValidationError(`${path}: null is not allowed`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeNode.primitive !== undefined) {
|
|
43
|
+
// A present, non-null value never satisfies a `primitive: 'null'` node — value
|
|
44
|
+
// being null is the only thing the branch above would have already returned for.
|
|
45
|
+
if (typeNode.primitive === 'null') {
|
|
46
|
+
throw new ValidationError(`${path}: expected null, got ${typeof value}`)
|
|
47
|
+
}
|
|
48
|
+
const expected = { string: 'string', integer: 'number', number: 'number', boolean: 'boolean' }[typeNode.primitive]
|
|
49
|
+
if (expected && typeof value !== expected) {
|
|
50
|
+
throw new ValidationError(`${path}: expected ${typeNode.primitive}, got ${typeof value}`)
|
|
51
|
+
}
|
|
52
|
+
// JSON has no representation for NaN/±Infinity — reject them for both numeric
|
|
53
|
+
// primitives before the integer-specific check narrows further. (Number.isInteger
|
|
54
|
+
// already excludes them too, so this is only load-bearing for a bare `number`.)
|
|
55
|
+
if ((typeNode.primitive === 'integer' || typeNode.primitive === 'number') && !Number.isFinite(value)) {
|
|
56
|
+
throw new ValidationError(`${path}: expected a finite ${typeNode.primitive}, got ${value}`)
|
|
57
|
+
}
|
|
58
|
+
// `number` admits any JSON number; `integer` rejects a fractional value
|
|
59
|
+
// (5.7) while still accepting one written 5.0 (Number.isInteger(5.0) is true).
|
|
60
|
+
if (typeNode.primitive === 'integer' && !Number.isInteger(value)) {
|
|
61
|
+
throw new ValidationError(`${path}: expected an integer, got ${value}`)
|
|
62
|
+
}
|
|
63
|
+
// An inline literal choice (project_bidi_schema.mjs's enumNode()) carries both
|
|
64
|
+
// `primitive` and `enum` — the primitive check above narrows the type, but the
|
|
65
|
+
// closed vocabulary below still needs to run, so only return early when there
|
|
66
|
+
// is no `enum` to fall through to.
|
|
67
|
+
if (typeNode.enum === undefined) return value
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeNode.const !== undefined) {
|
|
71
|
+
if (value !== typeNode.const) {
|
|
72
|
+
throw new ValidationError(
|
|
73
|
+
`${path}: expected constant ${JSON.stringify(typeNode.const)}, got ${JSON.stringify(value)}`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
return value
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeNode.enum !== undefined) {
|
|
80
|
+
if (!typeNode.enum.includes(value)) {
|
|
81
|
+
throw new ValidationError(
|
|
82
|
+
`${path}: "${value}" is not a valid value; expected one of: ${typeNode.enum.join(', ')}`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
return value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeNode.list !== undefined) {
|
|
89
|
+
if (!Array.isArray(value)) {
|
|
90
|
+
throw new ValidationError(`${path}: expected a list, got ${typeof value}`)
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze(value.map((item, i) => validateValue(typeNode.list, item, `${path}[${i}]`, direction)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeNode.map !== undefined) {
|
|
96
|
+
if (typeof value !== 'object' || Array.isArray(value) || value === null) {
|
|
97
|
+
throw new ValidationError(`${path}: expected an object, got ${typeof value}`)
|
|
98
|
+
}
|
|
99
|
+
// Object.create(null), not `{}`: `key` is wire-controlled and a literal "__proto__"
|
|
100
|
+
// entry assigned via bracket notation would hijack result's prototype instead of
|
|
101
|
+
// becoming a data property (CWE-1321) — a null-prototype object has no such trap.
|
|
102
|
+
const result = Object.create(null)
|
|
103
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
104
|
+
result[key] = validateValue(typeNode.map, entry, `${path}.${key}`, direction)
|
|
105
|
+
}
|
|
106
|
+
return Object.freeze(result)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeNode.ref !== undefined) {
|
|
110
|
+
const referenced = resolve(typeNode.ref)
|
|
111
|
+
if (referenced === undefined) return value // not yet registered — best-effort, skip deep validation
|
|
112
|
+
|
|
113
|
+
if (referenced.kind === 'enum') {
|
|
114
|
+
if (!referenced.includes(value)) {
|
|
115
|
+
throw new ValidationError(
|
|
116
|
+
`${path}: "${value}" is not a valid ${typeNode.ref} value; expected one of: ${referenced.values.join(', ')}`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return value
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (referenced.kind === 'record') {
|
|
123
|
+
if (value instanceof referenced.RecordClass) return value // already validated
|
|
124
|
+
if (typeof value !== 'object' || Array.isArray(value) || value === null) {
|
|
125
|
+
throw new ValidationError(`${path}: expected an object, got ${typeof value}`)
|
|
126
|
+
}
|
|
127
|
+
// Recurse through the same-direction path so a nested field gets the same
|
|
128
|
+
// tolerance (inbound) or strictness (outbound) as its parent. Inbound returns
|
|
129
|
+
// the parsed instance itself, so a typed parent record ends up with a typed
|
|
130
|
+
// nested value instead of the raw wire object; outbound only validates this
|
|
131
|
+
// way (the caller's own value is what gets kept, see the note above).
|
|
132
|
+
if (direction === 'inbound') {
|
|
133
|
+
return referenced.RecordClass.fromWire(value)
|
|
134
|
+
}
|
|
135
|
+
new referenced.RecordClass(value)
|
|
136
|
+
return value
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (referenced.kind === 'union') {
|
|
140
|
+
if (direction === 'inbound') {
|
|
141
|
+
return referenced.fromWire(value)
|
|
142
|
+
}
|
|
143
|
+
referenced.build(value)
|
|
144
|
+
return value
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (referenced.kind === 'alias') {
|
|
148
|
+
return validateValue(referenced.type, value, path, direction)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return value
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeNode.union !== undefined) {
|
|
155
|
+
const errors = []
|
|
156
|
+
for (const variant of typeNode.union) {
|
|
157
|
+
try {
|
|
158
|
+
return validateValue(variant, value, path, direction)
|
|
159
|
+
} catch (err) {
|
|
160
|
+
errors.push(err.message)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new ValidationError(`${path}: value did not match any variant (${errors.join('; ')})`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// An inline (unnamed) record type node — project_bidi_schema.mjs's projectEntry()
|
|
167
|
+
// emits this for an anonymous CDDL group (e.g. a field typed as an inline `{ ... }`
|
|
168
|
+
// rather than a ref to a named, defineRecord()'d type) — same FieldSpec shape as a
|
|
169
|
+
// named record's `fields`, just with nowhere to register a class. Gets the same
|
|
170
|
+
// directional required/extra/nested-value handling a named record's constructor/
|
|
171
|
+
// fromWire gives its fields, just built inline instead of through a Record class.
|
|
172
|
+
if (typeNode.record !== undefined) {
|
|
173
|
+
if (typeof value !== 'object' || Array.isArray(value) || value === null) {
|
|
174
|
+
throw new ValidationError(`${path}: expected an object, got ${typeof value}`)
|
|
175
|
+
}
|
|
176
|
+
const byWire = new Map(typeNode.record.map((f) => [f.wire, f]))
|
|
177
|
+
const result = {}
|
|
178
|
+
for (const field of typeNode.record) {
|
|
179
|
+
if (!Object.hasOwn(value, field.wire)) {
|
|
180
|
+
if (field.required) {
|
|
181
|
+
throw new ValidationError(`${path}.${field.wire}: required field is missing`)
|
|
182
|
+
}
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
result[field.name] = validateValue(field.type, value[field.wire], `${path}.${field.wire}`, direction)
|
|
186
|
+
}
|
|
187
|
+
for (const wireKey of Object.keys(value)) {
|
|
188
|
+
if (byWire.has(wireKey)) continue
|
|
189
|
+
// No `extensible` concept exists for an inline record (project_bidi_schema.mjs
|
|
190
|
+
// never sets it there) — undeclared keys get exactly the non-extensible named-
|
|
191
|
+
// record treatment: rejected outbound, dropped-with-a-warning inbound.
|
|
192
|
+
if (direction === 'inbound') {
|
|
193
|
+
process.emitWarning(`${path}: undeclared property "${wireKey}"`, 'BiDiSchemaWarning')
|
|
194
|
+
} else {
|
|
195
|
+
throw new ValidationError(`${path}: unknown property "${wireKey}"`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return Object.freeze(result)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return value
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Registers a schema `record` — a fixed set of named fields, each independently
|
|
206
|
+
* validated on the way out (constructor) and in (fromWire()).
|
|
207
|
+
* @param {string} name Schema type name, e.g. 'network.AddInterceptParameters'.
|
|
208
|
+
* @param {Array<{name: string, wire: string, required: boolean, type: object}>} fields
|
|
209
|
+
* @param {{extensible?: boolean}} [options]
|
|
210
|
+
* @returns {{new (data: object): object, fromWire: function(unknown): object}}
|
|
211
|
+
* The generated Record class — `new Record(data)` validates and constructs
|
|
212
|
+
* outbound, `Record.fromWire(payload)` validates and parses inbound.
|
|
213
|
+
*/
|
|
214
|
+
function defineRecord(name, fields, options = {}) {
|
|
215
|
+
const { extensible = false } = options
|
|
216
|
+
const byWire = new Map(fields.map((f) => [f.wire, f]))
|
|
217
|
+
// JS property name -> wire key, the inverse of byWire — lets toJSON() below map an
|
|
218
|
+
// outbound instance's own (JS-facing) properties back to the wire's declared names.
|
|
219
|
+
const byName = new Map(fields.map((f) => [f.name, f.wire]))
|
|
220
|
+
|
|
221
|
+
class Record {
|
|
222
|
+
// Outbound: strict. Any value that doesn't match its declared shape is an error here.
|
|
223
|
+
constructor(data) {
|
|
224
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
225
|
+
throw new ValidationError(`${name}: expected an object`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const field of fields) {
|
|
229
|
+
if (!Object.hasOwn(data, field.wire)) {
|
|
230
|
+
if (field.required) {
|
|
231
|
+
throw new ValidationError(`${name}.${field.wire}: required field is missing`)
|
|
232
|
+
}
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
const value = data[field.wire]
|
|
236
|
+
this[field.name] = validateValue(field.type, value, `${name}.${field.wire}`, 'outbound')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const wireKey of Object.keys(data)) {
|
|
240
|
+
if (byWire.has(wireKey)) continue
|
|
241
|
+
if (!extensible) {
|
|
242
|
+
throw new ValidationError(`${name}: unknown property "${wireKey}"`)
|
|
243
|
+
}
|
|
244
|
+
// Object.defineProperty, not `this[wireKey] = ...`: wireKey is caller-supplied
|
|
245
|
+
// and a literal "__proto__" key assigned via bracket notation hijacks this
|
|
246
|
+
// instance's actual prototype instead of becoming a field (CWE-1321).
|
|
247
|
+
// defineProperty always creates a genuine own data property, regardless of name.
|
|
248
|
+
Object.defineProperty(this, wireKey, {
|
|
249
|
+
value: data[wireKey], // vendor extras reach the wire on an extensible type
|
|
250
|
+
enumerable: true,
|
|
251
|
+
writable: true,
|
|
252
|
+
configurable: true,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
Object.freeze(this)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Inbound: tolerant of undeclared properties, but a missing required field
|
|
260
|
+
// is rejected just like a structurally invalid value — omission used to be
|
|
261
|
+
// tolerated here, but that's no longer required.
|
|
262
|
+
// Bypasses the constructor above entirely — a single constructor enforcing
|
|
263
|
+
// both directions symmetrically would make tolerated undeclared-property
|
|
264
|
+
// retention impossible.
|
|
265
|
+
static fromWire(payload) {
|
|
266
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
267
|
+
throw new ValidationError(`${name}: expected an object on the wire, got ${typeof payload}`)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const instance = Object.create(Record.prototype)
|
|
271
|
+
|
|
272
|
+
for (const field of fields) {
|
|
273
|
+
if (!Object.hasOwn(payload, field.wire)) {
|
|
274
|
+
if (field.required) {
|
|
275
|
+
throw new ValidationError(`${name}.${field.wire}: required field is missing`)
|
|
276
|
+
}
|
|
277
|
+
continue // left genuinely absent, optional field
|
|
278
|
+
}
|
|
279
|
+
const value = payload[field.wire]
|
|
280
|
+
// A present value's shape is never tolerated, inbound or outbound.
|
|
281
|
+
instance[field.name] = validateValue(field.type, value, `${name}.${field.wire}`, 'inbound')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const wireKey of Object.keys(payload)) {
|
|
285
|
+
if (byWire.has(wireKey)) continue
|
|
286
|
+
// An extensible type preserves an undeclared field silently — no narrower
|
|
287
|
+
// criterion than "extensible" itself (not, say, only fields that happen to be
|
|
288
|
+
// sendable back on some other type). A non-extensible type warns and drops it
|
|
289
|
+
// instead — the warning belongs only to the drop, not the retention.
|
|
290
|
+
if (extensible) {
|
|
291
|
+
// Object.defineProperty, not `instance[wireKey] = ...` — see the matching
|
|
292
|
+
// comment in the constructor above: a literal "__proto__" key from an
|
|
293
|
+
// untrusted wire payload must become a field, not swap the prototype (CWE-1321).
|
|
294
|
+
Object.defineProperty(instance, wireKey, {
|
|
295
|
+
value: payload[wireKey],
|
|
296
|
+
enumerable: true,
|
|
297
|
+
writable: true,
|
|
298
|
+
configurable: true,
|
|
299
|
+
})
|
|
300
|
+
} else {
|
|
301
|
+
process.emitWarning(`${name}: undeclared property "${wireKey}"`, 'BiDiSchemaWarning')
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
Object.freeze(instance)
|
|
306
|
+
return instance
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// The JSON.stringify hook: an instance stores its fields under their JS-facing
|
|
310
|
+
// property names (this[field.name]), but the wire needs the spec's own key
|
|
311
|
+
// (field.wire) — the two differ whenever a generator picks an idiomatic JS name
|
|
312
|
+
// distinct from the raw spec key. Runs automatically wherever this instance is
|
|
313
|
+
// serialized (directly, or nested inside another value being stringified), so a
|
|
314
|
+
// caller never has to remember to call it.
|
|
315
|
+
toJSON() {
|
|
316
|
+
// Object.create(null), not `{}`: an extra's key is wire-controlled (extensible
|
|
317
|
+
// types preserve undeclared properties verbatim, see the constructor above), and
|
|
318
|
+
// a literal "__proto__" key assigned via bracket notation would hijack wire's
|
|
319
|
+
// prototype instead of becoming a data property (CWE-1321) — same hazard the
|
|
320
|
+
// constructor/fromWire already guard against for the instance itself.
|
|
321
|
+
const wire = Object.create(null)
|
|
322
|
+
for (const key of Object.keys(this)) {
|
|
323
|
+
wire[byName.get(key) ?? key] = this[key] // extras have no JS-name mapping — already wire-keyed
|
|
324
|
+
}
|
|
325
|
+
return wire
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
Object.defineProperty(Record, 'name', { value: name })
|
|
330
|
+
register(name, { kind: 'record', RecordClass: Record })
|
|
331
|
+
return Record
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Registers a schema `alias` — a name with no fields of its own, just a
|
|
336
|
+
* pointer to another type node (e.g. `network.Intercept` aliasing a plain
|
|
337
|
+
* string). A ref to an alias validates through the aliased type node.
|
|
338
|
+
* @param {string} name
|
|
339
|
+
* @param {object} type The schema's `type` node this name aliases.
|
|
340
|
+
*/
|
|
341
|
+
function defineAlias(name, type) {
|
|
342
|
+
register(name, { kind: 'alias', type })
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = { defineRecord, defineAlias, ValidationError }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The SFC licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
|
|
18
|
+
// Shared type registry: every generated type registers itself here by its
|
|
19
|
+
// exact schema name (e.g. 'network.InterceptPhase'), so a field whose type is
|
|
20
|
+
// a `ref` can look up what the referenced type actually is — without every
|
|
21
|
+
// domain file needing to import every other domain file directly, and without
|
|
22
|
+
// needing types defined in dependency order (resolution happens at validation
|
|
23
|
+
// time, not at define time, so forward and circular refs both work).
|
|
24
|
+
const types = new Map()
|
|
25
|
+
|
|
26
|
+
function register(name, entry) {
|
|
27
|
+
types.set(name, entry)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolve(name) {
|
|
31
|
+
return types.get(name)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { register, resolve }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The SFC licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
|
|
18
|
+
const { register, resolve } = require('./registry')
|
|
19
|
+
const { ValidationError } = require('./record')
|
|
20
|
+
|
|
21
|
+
// Resolves the variant ref a value/payload matches, per the schema's selector shape:
|
|
22
|
+
// { by, variants: [{value, ref}], default? } - discriminated: match `data[by]` against
|
|
23
|
+
// each variant's value.
|
|
24
|
+
// { ordered: [{ref, requires}] } - structural: first variant whose `requires`
|
|
25
|
+
// keys are all present in `data`, in spec order.
|
|
26
|
+
function selectVariant(selector, data, hasKey) {
|
|
27
|
+
if (selector.by) {
|
|
28
|
+
const tag = hasKey(data, selector.by) ? data[selector.by] : undefined
|
|
29
|
+
const match = selector.variants.find((v) => v.value === tag)
|
|
30
|
+
if (match) return match.ref
|
|
31
|
+
return selector.default
|
|
32
|
+
}
|
|
33
|
+
if (selector.ordered) {
|
|
34
|
+
for (const variant of selector.ordered) {
|
|
35
|
+
if (variant.requires.every((key) => hasKey(data, key))) return variant.ref
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
return undefined // correlated: resolved by request id elsewhere, not from the payload
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registers a schema `union` — a value that may be any one of several variant
|
|
44
|
+
* record types, resolved by a discriminator field or by structural shape.
|
|
45
|
+
* @param {string} name Schema type name, e.g. 'session.ProxyConfiguration'.
|
|
46
|
+
* @param {object} selector The schema's `selector` node for this union.
|
|
47
|
+
* @param {{objectOnly?: boolean}} [options]
|
|
48
|
+
* @returns {{build: function(unknown): object, fromWire: function(unknown): object}}
|
|
49
|
+
* The registered union — `build(data)` resolves and constructs the matching
|
|
50
|
+
* variant outbound, `fromWire(payload)` resolves and parses it inbound.
|
|
51
|
+
*/
|
|
52
|
+
function defineUnion(name, selector, options = {}) {
|
|
53
|
+
const { objectOnly = false } = options
|
|
54
|
+
|
|
55
|
+
const union = {
|
|
56
|
+
kind: 'union',
|
|
57
|
+
|
|
58
|
+
// Outbound: resolve which variant `data` describes, then delegate to that
|
|
59
|
+
// variant's own (strict) constructor. A discriminated selector's `default`
|
|
60
|
+
// catch-all can itself resolve to another union — not just a record — e.g.
|
|
61
|
+
// LocalValue's untyped RemoteReference arm (see unionSelector() in
|
|
62
|
+
// project_bidi_schema.mjs) — so recurse through that union's own dispatch
|
|
63
|
+
// rather than assuming every resolved ref is a record.
|
|
64
|
+
build(data) {
|
|
65
|
+
if (objectOnly && (typeof data !== 'object' || data === null || Array.isArray(data))) {
|
|
66
|
+
throw new ValidationError(`${name}: expected an object`)
|
|
67
|
+
}
|
|
68
|
+
const ref = selectVariant(selector, data, (d, key) => Object.hasOwn(d, key))
|
|
69
|
+
if (ref === undefined) {
|
|
70
|
+
throw new ValidationError(`${name}: value does not match any known variant`)
|
|
71
|
+
}
|
|
72
|
+
const variant = resolve(ref)
|
|
73
|
+
if (variant.kind === 'union') {
|
|
74
|
+
return variant.build(data)
|
|
75
|
+
}
|
|
76
|
+
return new variant.RecordClass(data)
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Inbound: resolve which variant `payload` matches. An unresolvable payload is a
|
|
80
|
+
// closed-vocabulary miss — always an error, never a warning, since there is no
|
|
81
|
+
// valid typed object to fall back to. Same nested-union case as build() above.
|
|
82
|
+
fromWire(payload) {
|
|
83
|
+
if (objectOnly && (typeof payload !== 'object' || payload === null || Array.isArray(payload))) {
|
|
84
|
+
throw new ValidationError(`${name}: expected an object on the wire, got ${typeof payload}`)
|
|
85
|
+
}
|
|
86
|
+
const ref = selectVariant(selector, payload, (d, key) => Object.hasOwn(d, key))
|
|
87
|
+
if (ref === undefined) {
|
|
88
|
+
throw new ValidationError(`${name}: received a variant not in this binding's BiDi schema`)
|
|
89
|
+
}
|
|
90
|
+
const variant = resolve(ref)
|
|
91
|
+
if (variant.kind === 'union') {
|
|
92
|
+
return variant.fromWire(payload)
|
|
93
|
+
}
|
|
94
|
+
return variant.RecordClass.fromWire(payload)
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
register(name, union)
|
|
99
|
+
return union
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { defineUnion }
|
|
Binary file
|
|
Binary file
|