diff --git a/lib/constructs/operation.js b/lib/constructs/operation.js index 3a4d0e39..c0f12319 100644 --- a/lib/constructs/operation.js +++ b/lib/constructs/operation.js @@ -79,7 +79,12 @@ class Operation { requires.merge(parameterConversions.requires); str += parameterConversions.body; - if (overloads.every(overload => conversions[overload.operation.idlType.idlType])) { + if ( + overloads.every(overload => { + const { idlType } = overload.operation.idlType; + return conversions[idlType] || this.ctx._externalTypes.includes(idlType); + }) + ) { str += ` return ${callOn}.${implFunc}(${argsSpread}); `; diff --git a/lib/parameters.js b/lib/parameters.js index 123fd82a..fa198959 100644 --- a/lib/parameters.js +++ b/lib/parameters.js @@ -197,6 +197,22 @@ module.exports.generateOverloadConversions = function (ctx, typeOfOp, name, pare } } + for (const [importPath, types] of Object.entries(ctx.options.externalTypes)) { + for (const type of types) { + const ts = S.filter(o => isOrIncludes(ctx, o.typeList[d], t => t.idlType === type)); + if (ts.length) { + if (!requires.has(type)) { + requires.addRaw(type, `require("${importPath}").${type}`); + } + possibilities.push(` + if (curArg instanceof ${type}) { + ${continued(ts[0], i)} + } + `); + } + } + } + const callables = S.filter(o => { return isOrIncludes(ctx, o.typeList[d], t => ["Function", "VoidFunction"].includes(t.idlType)); }); diff --git a/lib/transformer.js b/lib/transformer.js index 592f96aa..beb591a1 100644 --- a/lib/transformer.js +++ b/lib/transformer.js @@ -18,10 +18,18 @@ class Transformer { this.ctx = new Context({ implSuffix: opts.implSuffix, options: { - suppressErrors: Boolean(opts.suppressErrors) + suppressErrors: Boolean(opts.suppressErrors), + externalTypes: opts.externalTypes } }); + this.ctx._externalTypes = []; + if (opts.externalTypes) { + for (const types of Object.values(opts.externalTypes)) { + this.ctx._externalTypes.push(...types); + } + } + this.sources = []; // Absolute paths to the IDL and Impl directories. this.utilPath = null; } diff --git a/lib/types.js b/lib/types.js index 87c978dc..e59b048b 100644 --- a/lib/types.js +++ b/lib/types.js @@ -117,6 +117,9 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e } else if (idlType.generic === "FrozenArray") { // frozen array type generateFrozenArray(); + } else if (ctx._externalTypes.includes(idlType.idlType)) { + // an external type, generate ad-hoc "conversion" + generateExternal(idlType.idlType); } else if (conversions[idlType.idlType]) { // string or number type compatible with webidl-conversions generateGeneric(`conversions["${idlType.idlType}"]`); @@ -210,6 +213,17 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e output.push(`if (${condition}) {}`); } + for (const [importPath, types] of Object.entries(ctx.options.externalTypes)) { + for (const type of types) { + if (union[type] || union.object) { + if (!requires.has(type)) { + requires.addRaw(type, `require("${importPath}").${type}`); + } + output.push(`if (${name} instanceof ${type}) {}`); + } + } + } + if (union.callback || union.object) { output.push(`if (typeof ${name} === "function") {}`); } @@ -352,6 +366,27 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e str += `${name} = Object.freeze(${name});`; } + function generateExternal(type) { + if (!requires.has(type)) { + let importPath; + for (const [path, types] of Object.entries(ctx.options.externalTypes)) { + if (types.includes(type)) { + importPath = path; + break; + } + } + if (importPath) { + requires.addRaw(type, `require("${importPath}").${type}`); + } + } + const article = /^[AEIOU]/.test(type) ? "an" : "a"; + str += ` + if (!(${name} instanceof ${type})) { + throw new TypeError(${errPrefix} + " is not ${article} ${type} object."); + } + `; + } + function generateGeneric(conversionFn) { const enforceRange = utils.getExtAttr(extAttrs, "EnforceRange"); const clamp = utils.getExtAttr(extAttrs, "Clamp"); @@ -410,6 +445,11 @@ function extractUnionInfo(ctx, idlType, errPrefix) { }, unknown: false }; + + for (const type of ctx._externalTypes) { + seen[type] = false; + } + for (const item of idlType.idlType) { if (item.generic === "sequence" || item.generic === "FrozenArray") { if (seen.sequenceLike) { @@ -437,6 +477,11 @@ function extractUnionInfo(ctx, idlType, errPrefix) { error(`${item.idlType} is not distinguishable with object type`); } seen.ArrayBufferViews.add(item.idlType); + } else if (ctx._externalTypes.includes(item.idlType)) { + if (seen.object) { + error(`${item.idlTypes} is not distinguishable with object type`); + } + seen[item.idlType] = true; } else if (stringTypes.has(item.idlType) || ctx.enumerations.has(item.idlType)) { if (seen.string) { error("There can only be one string type in a union type"); @@ -579,6 +624,7 @@ function sameType(ctx, type1, type2) { extracted1.ArrayBuffer !== extracted2.ArrayBuffer && JSON.stringify([...extracted1.ArrayBufferViews].sort()) === JSON.stringify([...extracted2.ArrayBufferViews].sort()) && + ctx._externalTypes.every(type => extracted1[type] === extracted2[type]) && extracted1.object === extracted2.object && sameType(ctx, extracted1.exception, extracted2.exception) && sameType(ctx, extracted1.string, extracted2.string) && diff --git a/test/__snapshots__/test.js.snap b/test/__snapshots__/test.js.snap index b0bb4f47..fca79f78 100644 --- a/test/__snapshots__/test.js.snap +++ b/test/__snapshots__/test.js.snap @@ -338,6 +338,341 @@ const Impl = require(\\"../implementations/Enum.js\\"); " `; +exports[`ExternalDict.webidl 1`] = ` +"\\"use strict\\"; + +const conversions = require(\\"webidl-conversions\\"); +const utils = require(\\"./utils.js\\"); + +const ReadableStream = require(\\"@platformparity/streams\\").ReadableStream; + +module.exports = { + convertInherit(obj, ret, { context = \\"The provided value\\" } = {}) { + { + const key = \\"rec\\"; + let value = obj === undefined || obj === null ? undefined : obj[key]; + if (value !== undefined) { + if (!utils.isObject(value)) { + throw new TypeError(context + \\" has member rec that\\" + \\" is not an object.\\"); + } else { + const result = Object.create(null); + for (const key of Reflect.ownKeys(value)) { + const desc = Object.getOwnPropertyDescriptor(value, key); + if (desc && desc.enumerable) { + let typedKey = key; + let typedValue = value[key]; + + typedKey = conversions[\\"USVString\\"](typedKey, { context: context + \\" has member rec that\\" + \\"'s key\\" }); + + if (!(typedValue instanceof ReadableStream)) { + throw new TypeError(context + \\" has member rec that\\" + \\"'s value\\" + \\" is not a ReadableStream object.\\"); + } + + result[typedKey] = typedValue; + } + } + value = result; + } + + ret[key] = value; + } + } + + { + const key = \\"seq\\"; + let value = obj === undefined || obj === null ? undefined : obj[key]; + if (value !== undefined) { + if (!utils.isObject(value)) { + throw new TypeError(context + \\" has member seq that\\" + \\" is not an iterable object.\\"); + } else { + const V = []; + const tmp = value; + for (let nextItem of tmp) { + if (!(nextItem instanceof ReadableStream)) { + throw new TypeError(context + \\" has member seq that\\" + \\"'s element\\" + \\" is not a ReadableStream object.\\"); + } + + V.push(nextItem); + } + value = V; + } + + ret[key] = value; + } + } + + { + const key = \\"stream\\"; + let value = obj === undefined || obj === null ? undefined : obj[key]; + if (value !== undefined) { + if (!(value instanceof ReadableStream)) { + throw new TypeError(context + \\" has member stream that\\" + \\" is not a ReadableStream object.\\"); + } + + ret[key] = value; + } + } + }, + + convert(obj, { context = \\"The provided value\\" } = {}) { + if (obj !== undefined && typeof obj !== \\"object\\" && typeof obj !== \\"function\\") { + throw new TypeError(\`\${context} is not an object.\`); + } + + const ret = Object.create(null); + module.exports.convertInherit(obj, ret, { context }); + return ret; + } +}; +" +`; + +exports[`ExternalTypes.webidl 1`] = ` +"\\"use strict\\"; + +const conversions = require(\\"webidl-conversions\\"); +const utils = require(\\"./utils.js\\"); + +const ReadableStream = require(\\"@platformparity/streams\\").ReadableStream; +const impl = utils.implSymbol; + +class ExternalTypes { + constructor() { + throw new TypeError(\\"Illegal constructor\\"); + } + + op(a) { + if (!this || !module.exports.is(this)) { + throw new TypeError(\\"Illegal invocation\\"); + } + + if (arguments.length < 1) { + throw new TypeError( + \\"Failed to execute 'op' on 'ExternalTypes': 1 argument required, but only \\" + arguments.length + \\" present.\\" + ); + } + const args = []; + { + let curArg = arguments[0]; + if (!(curArg instanceof ReadableStream)) { + throw new TypeError( + \\"Failed to execute 'op' on 'ExternalTypes': parameter 1\\" + \\" is not a ReadableStream object.\\" + ); + } + args.push(curArg); + } + return this[impl].op(...args); + } +} +Object.defineProperties(ExternalTypes.prototype, { + op: { enumerable: true }, + [Symbol.toStringTag]: { value: \\"ExternalTypes\\", configurable: true } +}); +const iface = { + // When an interface-module that implements this interface as a mixin is loaded, it will append its own \`.is()\` + // method into this array. It allows objects that directly implements *those* interfaces to be recognized as + // implementing this mixin interface. + _mixedIntoPredicates: [], + is(obj) { + if (obj) { + if (utils.hasOwn(obj, impl) && obj[impl] instanceof Impl.implementation) { + return true; + } + for (const isMixedInto of module.exports._mixedIntoPredicates) { + if (isMixedInto(obj)) { + return true; + } + } + } + return false; + }, + isImpl(obj) { + if (obj) { + if (obj instanceof Impl.implementation) { + return true; + } + + const wrapper = utils.wrapperForImpl(obj); + for (const isMixedInto of module.exports._mixedIntoPredicates) { + if (isMixedInto(wrapper)) { + return true; + } + } + } + return false; + }, + convert(obj, { context = \\"The provided value\\" } = {}) { + if (module.exports.is(obj)) { + return utils.implForWrapper(obj); + } + throw new TypeError(\`\${context} is not of type 'ExternalTypes'.\`); + }, + + create(constructorArgs, privateData) { + let obj = Object.create(ExternalTypes.prototype); + obj = this.setup(obj, constructorArgs, privateData); + return obj; + }, + createImpl(constructorArgs, privateData) { + let obj = Object.create(ExternalTypes.prototype); + obj = this.setup(obj, constructorArgs, privateData); + return utils.implForWrapper(obj); + }, + _internalSetup(obj) {}, + setup(obj, constructorArgs, privateData) { + if (!privateData) privateData = {}; + + privateData.wrapper = obj; + + this._internalSetup(obj); + Object.defineProperty(obj, impl, { + value: new Impl.implementation(constructorArgs, privateData), + configurable: true + }); + + obj[impl][utils.wrapperSymbol] = obj; + if (Impl.init) { + Impl.init(obj[impl], privateData); + } + return obj; + }, + interface: ExternalTypes, + expose: { + Window: { ExternalTypes } + } +}; // iface +module.exports = iface; + +const Impl = require(\\"../implementations/ExternalTypes.js\\"); +" +`; + +exports[`ExternalTypesUnion.webidl 1`] = ` +"\\"use strict\\"; + +const conversions = require(\\"webidl-conversions\\"); +const utils = require(\\"./utils.js\\"); + +const isURLSearchParams = require(\\"./URLSearchParams.js\\").is; +const ReadableStream = require(\\"@platformparity/streams\\").ReadableStream; +const impl = utils.implSymbol; + +class ExternalTypesUnion { + constructor() { + throw new TypeError(\\"Illegal constructor\\"); + } + + op(a) { + if (!this || !module.exports.is(this)) { + throw new TypeError(\\"Illegal invocation\\"); + } + + if (arguments.length < 1) { + throw new TypeError( + \\"Failed to execute 'op' on 'ExternalTypesUnion': 1 argument required, but only \\" + + arguments.length + + \\" present.\\" + ); + } + const args = []; + { + let curArg = arguments[0]; + if (isURLSearchParams(curArg)) { + curArg = utils.implForWrapper(curArg); + } else if (curArg instanceof ReadableStream) { + } else { + curArg = conversions[\\"USVString\\"](curArg, { + context: \\"Failed to execute 'op' on 'ExternalTypesUnion': parameter 1\\" + }); + } + args.push(curArg); + } + return utils.tryWrapperForImpl(this[impl].op(...args)); + } +} +Object.defineProperties(ExternalTypesUnion.prototype, { + op: { enumerable: true }, + [Symbol.toStringTag]: { value: \\"ExternalTypesUnion\\", configurable: true } +}); +const iface = { + // When an interface-module that implements this interface as a mixin is loaded, it will append its own \`.is()\` + // method into this array. It allows objects that directly implements *those* interfaces to be recognized as + // implementing this mixin interface. + _mixedIntoPredicates: [], + is(obj) { + if (obj) { + if (utils.hasOwn(obj, impl) && obj[impl] instanceof Impl.implementation) { + return true; + } + for (const isMixedInto of module.exports._mixedIntoPredicates) { + if (isMixedInto(obj)) { + return true; + } + } + } + return false; + }, + isImpl(obj) { + if (obj) { + if (obj instanceof Impl.implementation) { + return true; + } + + const wrapper = utils.wrapperForImpl(obj); + for (const isMixedInto of module.exports._mixedIntoPredicates) { + if (isMixedInto(wrapper)) { + return true; + } + } + } + return false; + }, + convert(obj, { context = \\"The provided value\\" } = {}) { + if (module.exports.is(obj)) { + return utils.implForWrapper(obj); + } + throw new TypeError(\`\${context} is not of type 'ExternalTypesUnion'.\`); + }, + + create(constructorArgs, privateData) { + let obj = Object.create(ExternalTypesUnion.prototype); + obj = this.setup(obj, constructorArgs, privateData); + return obj; + }, + createImpl(constructorArgs, privateData) { + let obj = Object.create(ExternalTypesUnion.prototype); + obj = this.setup(obj, constructorArgs, privateData); + return utils.implForWrapper(obj); + }, + _internalSetup(obj) {}, + setup(obj, constructorArgs, privateData) { + if (!privateData) privateData = {}; + + privateData.wrapper = obj; + + this._internalSetup(obj); + Object.defineProperty(obj, impl, { + value: new Impl.implementation(constructorArgs, privateData), + configurable: true + }); + + obj[impl][utils.wrapperSymbol] = obj; + if (Impl.init) { + Impl.init(obj[impl], privateData); + } + return obj; + }, + interface: ExternalTypesUnion, + expose: { + Window: { ExternalTypesUnion } + } +}; // iface +module.exports = iface; + +const Impl = require(\\"../implementations/ExternalTypesUnion.js\\"); +" +`; + exports[`Factory.webidl 1`] = ` "\\"use strict\\"; diff --git a/test/cases/ExternalDict.webidl b/test/cases/ExternalDict.webidl new file mode 100644 index 00000000..6d86027e --- /dev/null +++ b/test/cases/ExternalDict.webidl @@ -0,0 +1,5 @@ +dictionary ExternalDict { + ReadableStream stream; + sequence seq; + record rec; +}; diff --git a/test/cases/ExternalTypes.webidl b/test/cases/ExternalTypes.webidl new file mode 100644 index 00000000..447db356 --- /dev/null +++ b/test/cases/ExternalTypes.webidl @@ -0,0 +1,3 @@ +interface ExternalTypes { + ReadableStream op(ReadableStream a); +}; diff --git a/test/cases/ExternalTypesUnion.webidl b/test/cases/ExternalTypesUnion.webidl new file mode 100644 index 00000000..b480b0ab --- /dev/null +++ b/test/cases/ExternalTypesUnion.webidl @@ -0,0 +1,4 @@ +typedef (URLSearchParams or ReadableStream or USVString) BodyInit; +interface ExternalTypesUnion { + BodyInit op(BodyInit a); +}; diff --git a/test/test.js b/test/test.js index 35adcdca..c2b6e127 100644 --- a/test/test.js +++ b/test/test.js @@ -12,7 +12,11 @@ const implsDir = path.resolve(__dirname, "implementations"); const outputDir = path.resolve(__dirname, "output"); beforeAll(() => { - const transformer = new Transformer(); + const transformer = new Transformer({ + externalTypes: { + "@platformparity/streams": ["ReadableStream"] + } + }); transformer.addSource(casesDir, implsDir); return transformer.generate(outputDir);