feat(rpc): client-side parameters validation (#3069)

This commit is contained in:
Dmitry Gozman 2020-07-22 18:05:07 -07:00 committed by GitHub
parent e56e148597
commit b1a5a02154
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1685 additions and 60 deletions

View file

@ -387,9 +387,9 @@ export class Page extends EventEmitter {
async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null }) { async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null }) {
if (options.media !== undefined) if (options.media !== undefined)
assert(options.media === null || types.mediaTypes.has(options.media), 'media: expected one of (screen|print)'); assert(options.media === null || types.mediaTypes.has(options.media), 'media: expected one of (screen|print|null)');
if (options.colorScheme !== undefined) if (options.colorScheme !== undefined)
assert(options.colorScheme === null || types.colorSchemes.has(options.colorScheme), 'colorScheme: expected one of (dark|light|no-preference)'); assert(options.colorScheme === null || types.colorSchemes.has(options.colorScheme), 'colorScheme: expected one of (dark|light|no-preference|null)');
if (options.media !== undefined) if (options.media !== undefined)
this._state.mediaType = options.media; this._state.mediaType = options.media;
if (options.colorScheme !== undefined) if (options.colorScheme !== undefined)

View file

@ -620,8 +620,8 @@ export type PageCloseParams = {
}; };
export type PageCloseResult = void; export type PageCloseResult = void;
export type PageEmulateMediaParams = { export type PageEmulateMediaParams = {
media?: 'screen' | 'print' | 'reset', media?: 'screen' | 'print' | 'null',
colorScheme?: 'dark' | 'light' | 'no-preference' | 'reset', colorScheme?: 'dark' | 'light' | 'no-preference' | 'null',
}; };
export type PageEmulateMediaResult = void; export type PageEmulateMediaResult = void;
export type PageExposeBindingParams = { export type PageExposeBindingParams = {

View file

@ -23,6 +23,7 @@ import { BrowserServer } from './browserServer';
import { LoggerSink } from '../../loggerSink'; import { LoggerSink } from '../../loggerSink';
import { headersObjectToArray, envObjectToArray } from '../serializers'; import { headersObjectToArray, envObjectToArray } from '../serializers';
import { serializeArgument } from './jsHandle'; import { serializeArgument } from './jsHandle';
import { assert } from '../../helper';
type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } }; type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
@ -48,6 +49,8 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
const logger = options.logger; const logger = options.logger;
options = { ...options, logger: undefined }; options = { ...options, logger: undefined };
return this._wrapApiCall('browserType.launch', async () => { return this._wrapApiCall('browserType.launch', async () => {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
const launchOptions: BrowserTypeLaunchParams = { const launchOptions: BrowserTypeLaunchParams = {
...options, ...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,

View file

@ -66,7 +66,7 @@ export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}
return obj.addListener; return obj.addListener;
if (prop === 'removeEventListener') if (prop === 'removeEventListener')
return obj.removeListener; return obj.removeListener;
return (params: any) => this._connection.sendMessageToServer({ guid, method: String(prop), params }); return (params: any) => this._connection.sendMessageToServer(this._type, guid, String(prop), params);
}, },
}); });
(this._channel as any)._object = this; (this._channel as any)._object = this;

View file

@ -38,6 +38,7 @@ import { ChromiumBrowser } from './chromiumBrowser';
import { ChromiumBrowserContext } from './chromiumBrowserContext'; import { ChromiumBrowserContext } from './chromiumBrowserContext';
import { Selectors } from './selectors'; import { Selectors } from './selectors';
import { Stream } from './stream'; import { Stream } from './stream';
import { validateParams } from './validator';
class Root extends ChannelOwner<Channel, {}> { class Root extends ChannelOwner<Channel, {}> {
constructor(connection: Connection) { constructor(connection: Connection) {
@ -63,9 +64,10 @@ export class Connection {
return new Promise(f => this._waitingForObject.set(guid, f)); return new Promise(f => this._waitingForObject.set(guid, f));
} }
async sendMessageToServer(message: { guid: string, method: string, params: any }): Promise<any> { async sendMessageToServer(type: string, guid: string, method: string, params: any): Promise<any> {
const id = ++this._lastId; const id = ++this._lastId;
const converted = { id, ...message, params: this._replaceChannelsWithGuids(message.params) }; const validated = method === 'debugScopeState' ? params : validateParams(type, method, params);
const converted = { id, guid, method, params: validated };
debug('pw:channel:command')(converted); debug('pw:channel:command')(converted);
this.onmessage(converted); this.onmessage(converted);
return new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject })); return new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject }));
@ -97,22 +99,6 @@ export class Connection {
object._channel.emit(method, this._replaceGuidsWithChannels(params)); object._channel.emit(method, this._replaceGuidsWithChannels(params));
} }
private _replaceChannelsWithGuids(payload: any): any {
if (!payload)
return payload;
if (Array.isArray(payload))
return payload.map(p => this._replaceChannelsWithGuids(p));
if (payload._object instanceof ChannelOwner)
return { guid: payload._object._guid };
if (typeof payload === 'object') {
const result: any = {};
for (const key of Object.keys(payload))
result[key] = this._replaceChannelsWithGuids(payload[key]);
return result;
}
return payload;
}
private _replaceGuidsWithChannels(payload: any): any { private _replaceGuidsWithChannels(payload: any): any {
if (!payload) if (!payload)
return payload; return payload;

View file

@ -123,7 +123,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
}); });
} }
async selectText(options: types.TimeoutOptions): Promise<void> { async selectText(options: types.TimeoutOptions = {}): Promise<void> {
return this._wrapApiCall('elementHandle.selectText', async () => { return this._wrapApiCall('elementHandle.selectText', async () => {
await this._elementChannel.selectText(options); await this._elementChannel.selectText(options);
}); });

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { assertMaxArguments, helper } from '../../helper'; import { assertMaxArguments, helper, assert } from '../../helper';
import * as types from '../../types'; import * as types from '../../types';
import { FrameChannel, FrameInitializer, FrameNavigatedEvent } from '../channels'; import { FrameChannel, FrameInitializer, FrameNavigatedEvent } from '../channels';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
@ -89,7 +89,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> { async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
return this._wrapApiCall(this._apiName('goto'), async () => { return this._wrapApiCall(this._apiName('goto'), async () => {
return network.Response.fromNullable((await this._channel.goto({ url, ...options })).response); const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
return network.Response.fromNullable((await this._channel.goto({ url, ...options, waitUntil })).response);
}); });
} }
@ -188,6 +189,10 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise<ElementHandle<Element> | null> { async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise<ElementHandle<Element> | null> {
return this._wrapApiCall(this._apiName('waitForSelector'), async () => { return this._wrapApiCall(this._apiName('waitForSelector'), async () => {
if ((options as any).visibility)
throw new Error('options.visibility is not supported, did you mean options.state?');
if ((options as any).waitFor && (options as any).waitFor !== 'visible')
throw new Error('options.waitFor is not supported, did you mean options.state?');
const result = await this._channel.waitForSelector({ selector, ...options }); const result = await this._channel.waitForSelector({ selector, ...options });
return ElementHandle.fromNullable(result.element) as ElementHandle<Element> | null; return ElementHandle.fromNullable(result.element) as ElementHandle<Element> | null;
}); });
@ -234,7 +239,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> { async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
return this._wrapApiCall(this._apiName('setContent'), async () => { return this._wrapApiCall(this._apiName('setContent'), async () => {
await this._channel.setContent({ html, ...options }); const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
await this._channel.setContent({ html, ...options, waitUntil });
}); });
} }
@ -272,9 +278,11 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> { async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<ElementHandle> {
return this._wrapApiCall(this._apiName('addStyleTag'), async () => { return this._wrapApiCall(this._apiName('addStyleTag'), async () => {
const copy = { ...options }; const copy = { ...options };
if (copy.path) if (copy.path) {
copy.content = (await fsReadFileAsync(copy.path)).toString(); copy.content = (await fsReadFileAsync(copy.path)).toString();
return ElementHandle.from((await this._channel.addStyleTag({ ...options })).element); copy.content += '/*# sourceURL=' + copy.path.replace(/\n/g, '') + '*/';
}
return ElementHandle.from((await this._channel.addStyleTag({ ...copy })).element);
}); });
} }
@ -378,6 +386,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async waitForFunction<R>(pageFunction: Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<SmartHandle<R>>; async waitForFunction<R>(pageFunction: Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<SmartHandle<R>>;
async waitForFunction<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise<SmartHandle<R>> { async waitForFunction<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise<SmartHandle<R>> {
return this._wrapApiCall(this._apiName('waitForFunction'), async () => { return this._wrapApiCall(this._apiName('waitForFunction'), async () => {
if (typeof options.polling === 'string')
assert(options.polling === 'raf', 'Unknown polling option: ' + options.polling);
const result = await this._channel.waitForFunction({ const result = await this._channel.waitForFunction({
...options, ...options,
pollingInterval: options.polling === 'raf' ? undefined : options.polling, pollingInterval: options.polling === 'raf' ? undefined : options.polling,
@ -396,7 +406,7 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
} }
} }
function verifyLoadState(name: string, waitUntil: types.LifecycleEvent): types.LifecycleEvent { export function verifyLoadState(name: string, waitUntil: types.LifecycleEvent): types.LifecycleEvent {
if (waitUntil as unknown === 'networkidle0') if (waitUntil as unknown === 'networkidle0')
waitUntil = 'networkidle'; waitUntil = 'networkidle';
if (!types.kLifecycleEvents.has(waitUntil)) if (!types.kLifecycleEvents.has(waitUntil))

View file

@ -29,7 +29,7 @@ import { Dialog } from './dialog';
import { Download } from './download'; import { Download } from './download';
import { ElementHandle } from './elementHandle'; import { ElementHandle } from './elementHandle';
import { Worker } from './worker'; import { Worker } from './worker';
import { Frame, FunctionWithSource, GotoOptions } from './frame'; import { Frame, FunctionWithSource, GotoOptions, verifyLoadState } from './frame';
import { Keyboard, Mouse } from './input'; import { Keyboard, Mouse } from './input';
import { Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle'; import { Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
import { Request, Response, Route, RouteHandler } from './network'; import { Request, Response, Route, RouteHandler } from './network';
@ -300,7 +300,8 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
async reload(options: types.NavigateOptions = {}): Promise<Response | null> { async reload(options: types.NavigateOptions = {}): Promise<Response | null> {
return this._wrapApiCall('page.reload', async () => { return this._wrapApiCall('page.reload', async () => {
return Response.fromNullable((await this._channel.reload(options)).response); const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
return Response.fromNullable((await this._channel.reload({ ...options, waitUntil })).response);
}); });
} }
@ -346,21 +347,23 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
async goBack(options: types.NavigateOptions = {}): Promise<Response | null> { async goBack(options: types.NavigateOptions = {}): Promise<Response | null> {
return this._wrapApiCall('page.goBack', async () => { return this._wrapApiCall('page.goBack', async () => {
return Response.fromNullable((await this._channel.goBack(options)).response); const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
return Response.fromNullable((await this._channel.goBack({ ...options, waitUntil })).response);
}); });
} }
async goForward(options: types.NavigateOptions = {}): Promise<Response | null> { async goForward(options: types.NavigateOptions = {}): Promise<Response | null> {
return this._wrapApiCall('page.goForward', async () => { return this._wrapApiCall('page.goForward', async () => {
return Response.fromNullable((await this._channel.goForward(options)).response); const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
return Response.fromNullable((await this._channel.goForward({ ...options, waitUntil })).response);
}); });
} }
async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null }) { async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null }) {
return this._wrapApiCall('page.emulateMedia', async () => { return this._wrapApiCall('page.emulateMedia', async () => {
await this._channel.emulateMedia({ await this._channel.emulateMedia({
media: options.media === null ? 'reset' : options.media, media: options.media === null ? 'null' : options.media,
colorScheme: options.colorScheme === null ? 'reset' : options.colorScheme, colorScheme: options.colorScheme === null ? 'null' : options.colorScheme,
}); });
}); });
} }

1391
src/rpc/client/validator.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,123 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChannelOwner } from './channelOwner';
import { isUnderTest } from '../../helper';
class ValidationError extends Error {}
export function validateParams(type: string, method: string, params: any): any {
const name = type + method[0].toUpperCase() + method.substring(1) + 'Params';
if (!scheme[name])
throw new ValidationError(`Uknown scheme for ${type}.${method}`);
return scheme[name](params, '');
}
type Validator = (arg: any, path: string) => any;
export const tNumber: Validator = (arg: any, path: string) => {
if (arg instanceof Number)
return arg.valueOf();
if (typeof arg === 'number')
return arg;
throw new ValidationError(`${path}: expected number, got ${typeof arg}`);
};
export const tBoolean: Validator = (arg: any, path: string) => {
if (arg instanceof Boolean)
return arg.valueOf();
if (typeof arg === 'boolean')
return arg;
throw new ValidationError(`${path}: expected boolean, got ${typeof arg}`);
};
export const tString: Validator = (arg: any, path: string) => {
if (arg instanceof String)
return arg.valueOf();
if (typeof arg === 'string')
return arg;
throw new ValidationError(`${path}: expected string, got ${typeof arg}`);
};
export const tBinary: Validator = (arg: any, path: string) => {
// TODO: convert from Buffer here.
if (arg instanceof String)
return arg.valueOf();
if (typeof arg === 'string')
return arg;
throw new ValidationError(`${path}: expected base64-encoded buffer, got ${typeof arg}`);
};
export const tUndefined: Validator = (arg: any, path: string) => {
if (Object.is(arg, undefined))
return arg;
throw new ValidationError(`${path}: expected undefined, got ${typeof arg}`);
};
export const tOptional = (v: Validator): Validator => {
return (arg: any, path: string) => {
if (Object.is(arg, undefined))
return arg;
return v(arg, path);
};
};
export const tArray = (v: Validator): Validator => {
return (arg: any, path: string) => {
if (!Array.isArray(arg))
throw new ValidationError(`${path}: expected array, got ${typeof arg}`);
return arg.map((x, index) => v(x, path + '[' + index + ']'));
};
};
export const tObject = (s: { [key: string]: Validator }): Validator => {
return (arg: any, path: string) => {
if (Object.is(arg, null))
throw new ValidationError(`${path}: expected object, got null`);
if (typeof arg !== 'object')
throw new ValidationError(`${path}: expected object, got ${typeof arg}`);
const result: any = {};
for (const [key, v] of Object.entries(s)) {
const value = v(arg[key], path ? path + '.' + key : key);
if (!Object.is(value, undefined))
result[key] = value;
}
if (isUnderTest()) {
for (const [key, value] of Object.entries(arg)) {
if (key.startsWith('__testHook'))
result[key] = value;
}
}
return result;
};
};
export const tEnum = (e: string[]): Validator => {
return (arg: any, path: string) => {
if (!e.includes(arg))
throw new ValidationError(`${path}: expected one of (${e.join('|')})`);
return arg;
};
};
export const tChannel = (name: string): Validator => {
return (arg: any, path: string) => {
if (arg._object instanceof ChannelOwner && (name === '*' || arg._object._type === name))
return { guid: arg._object._guid };
throw new ValidationError(`${path}: expected ${name}`);
};
};
export const tType = (name: string): Validator => {
return (arg: any, path: string) => {
const v = scheme[name];
if (!v)
throw new ValidationError(`${path}: unknown type "${name}"`);
return v(arg, path);
};
};
export const scheme: { [key: string]: Validator } = {};

View file

@ -530,13 +530,13 @@ interface Page
screen screen
print print
# Reset emulated value to the system default. # Reset emulated value to the system default.
reset null
colorScheme?: enum colorScheme?: enum
dark dark
light light
no-preference no-preference
# Reset emulated value to the system default. # Reset emulated value to the system default.
reset null
command exposeBinding command exposeBinding
parameters parameters

View file

@ -108,8 +108,8 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
async emulateMedia(params: PageEmulateMediaParams): Promise<void> { async emulateMedia(params: PageEmulateMediaParams): Promise<void> {
await this._page.emulateMedia({ await this._page.emulateMedia({
media: params.media === 'reset' ? null : params.media, media: params.media === 'null' ? null : params.media,
colorScheme: params.colorScheme === 'reset' ? null : params.colorScheme, colorScheme: params.colorScheme === 'null' ? null : params.colorScheme,
}); });
} }

View file

@ -248,7 +248,7 @@ describe('Page.emulateMedia type', function() {
it('should throw in case of bad type argument', async({page, server}) => { it('should throw in case of bad type argument', async({page, server}) => {
let error = null; let error = null;
await page.emulateMedia({ media: 'bad' }).catch(e => error = e); await page.emulateMedia({ media: 'bad' }).catch(e => error = e);
expect(error.message).toContain('media: expected one of (screen|print)'); expect(error.message).toContain('media: expected one of (screen|print|null)');
}); });
}); });
@ -276,7 +276,7 @@ describe('Page.emulateMedia colorScheme', function() {
it('should throw in case of bad argument', async({page, server}) => { it('should throw in case of bad argument', async({page, server}) => {
let error = null; let error = null;
await page.emulateMedia({ colorScheme: 'bad' }).catch(e => error = e); await page.emulateMedia({ colorScheme: 'bad' }).catch(e => error = e);
expect(error.message).toContain('colorScheme: expected one of (dark|light|no-preference)'); expect(error.message).toContain('colorScheme: expected one of (dark|light|no-preference|null)');
}); });
it('should work during navigation', async({page, server}) => { it('should work during navigation', async({page, server}) => {
await page.emulateMedia({ colorScheme: 'light' }); await page.emulateMedia({ colorScheme: 'light' });

View file

@ -24,6 +24,8 @@ const { Connection } = require('../../lib/rpc/client/connection');
const { Transport } = require('../../lib/rpc/transport'); const { Transport } = require('../../lib/rpc/transport');
const { PlaywrightDispatcher } = require('../../lib/rpc/server/playwrightDispatcher'); const { PlaywrightDispatcher } = require('../../lib/rpc/server/playwrightDispatcher');
const { setUseApiName } = require('../../lib/progress'); const { setUseApiName } = require('../../lib/progress');
const { setUnderTest } = require('../../lib/helper');
setUnderTest();
const browserName = process.env.BROWSER || 'chromium'; const browserName = process.env.BROWSER || 'chromium';

View file

@ -76,7 +76,7 @@ describe('Playwright', function() {
const error = await browserType.launch(options).catch(e => e); const error = await browserType.launch(options).catch(e => e);
expect(error.message).toContain('<launching>'); expect(error.message).toContain('<launching>');
}); });
it.skip(CHANNEL).slow()('should accept objects as options', async({browserType, defaultBrowserOptions}) => { it.slow()('should accept objects as options', async({browserType, defaultBrowserOptions}) => {
const browser = await browserType.launch({ ...defaultBrowserOptions, process }); const browser = await browserType.launch({ ...defaultBrowserOptions, process });
await browser.close(); await browser.close();
}); });

View file

@ -72,8 +72,6 @@ function inlineType(item, indent) {
inner = `{\n${properties(item, indent + ' ')}\n${indent}}`; inner = `{\n${properties(item, indent + ' ')}\n${indent}}`;
} else if (type === 'binary') { } else if (type === 'binary') {
inner = 'Binary'; inner = 'Binary';
} else if (type === 'Error') {
inner = 'SerializedError';
} else if (channels.has(type)) { } else if (channels.has(type)) {
inner = type + 'Channel'; inner = type + 'Channel';
} else { } else {
@ -82,6 +80,35 @@ function inlineType(item, indent) {
return inner + (array ? '[]' : ''); return inner + (array ? '[]' : '');
} }
function inlineTypeScheme(item, indent) {
let type = item.words[1];
const array = type.endsWith('[]');
if (array)
type = type.substring(0, type.length - 2);
let inner = '';
if (type === 'enum') {
const literals = item.list.map(literal => {
if (literal.words.length > 1 || literal.list.length)
raise(literal);
return literal.words[0];
});
inner = `tEnum([${literals.map(literal => `'${literal}'`).join(', ')}])`;
} else if (['string', 'boolean', 'number', 'undefined'].includes(type)) {
inner = `t${titleCase(type)}`;
} else if (type === 'object') {
inner = `tObject({\n${propertiesScheme(item, indent + ' ')}\n${indent}})`;
} else if (type === 'binary') {
inner = 'tBinary';
} else if (channels.has(type)) {
inner = `tChannel('${type}')`;
} else if (type === 'Channel') {
inner = `tChannel('*')`;
} else {
inner = `tType('${type}')`;
}
return array ? `tArray(${inner})` : inner;
}
function properties(item, indent) { function properties(item, indent) {
const result = []; const result = [];
for (const prop of item.list) { for (const prop of item.list) {
@ -99,13 +126,39 @@ function properties(item, indent) {
return result.join('\n'); return result.join('\n');
} }
function propertiesScheme(item, indent) {
const result = [];
for (const prop of item.list) {
if (prop.words.length !== 2)
raise(prop);
let name = prop.words[0];
if (!name.endsWith(':'))
raise(item);
name = name.substring(0, name.length - 1);
const optional = name.endsWith('?');
if (optional)
name = name.substring(0, name.length - 1);
let type = inlineTypeScheme(prop, indent);
if (optional)
type = `tOptional(${type})`;
result.push(`${indent}${name}: ${type},`);
}
return result.join('\n');
}
function objectType(name, item, indent) { function objectType(name, item, indent) {
if (!item.list.length) if (!item.list.length)
return `export type ${name} = {};`; return `export type ${name} = {};`;
return `export type ${name} = {\n${properties(item, indent)}\n};` return `export type ${name} = {\n${properties(item, indent)}\n};`
} }
const result = [ function objectTypeScheme(item, indent) {
if (!item.list.length)
return `tObject({})`;
return `tObject({\n${propertiesScheme(item, indent)}\n})`
}
const channels_ts = [
`/** `/**
* Copyright (c) Microsoft Corporation. * Copyright (c) Microsoft Corporation.
* *
@ -134,33 +187,46 @@ export interface Channel extends EventEmitter {
const pdl = fs.readFileSync(path.join(__dirname, '..', 'src', 'rpc', 'protocol.pdl'), 'utf-8'); const pdl = fs.readFileSync(path.join(__dirname, '..', 'src', 'rpc', 'protocol.pdl'), 'utf-8');
const list = tokenize(pdl); const list = tokenize(pdl);
const scheme = new Map();
const inherits = new Map();
function addScheme(name, s) {
if (scheme.has(name))
throw new Error('Duplicate scheme name ' + name);
scheme.set(name, s);
}
for (const item of list) { for (const item of list) {
if (item.words[0] === 'interface') if (item.words[0] === 'interface') {
channels.add(item.words[1]); channels.add(item.words[1]);
if (item.words[2] === 'extends')
inherits.set(item.words[1], item.words[3]);
}
} }
for (const item of list) { for (const item of list) {
if (item.words[0] === 'type') { if (item.words[0] === 'type') {
if (item.words.length !== 2) if (item.words.length !== 2)
raise(item); raise(item);
result.push(`export type ${item.words[1]} = {`); channels_ts.push(`export type ${item.words[1]} = {`);
result.push(properties(item, ' ')); channels_ts.push(properties(item, ' '));
result.push(`};`); channels_ts.push(`};`);
addScheme(item.words[1], objectTypeScheme(item, ' '));
} else if (item.words[0] === 'interface') { } else if (item.words[0] === 'interface') {
const channelName = item.words[1]; const channelName = item.words[1];
result.push(`// ----------- ${channelName} -----------`); channels_ts.push(`// ----------- ${channelName} -----------`);
const init = item.list.find(i => i.words[0] === 'initializer'); const init = item.list.find(i => i.words[0] === 'initializer');
if (init && init.words.length > 1) if (init && init.words.length > 1)
raise(init); raise(init);
result.push(objectType(channelName + 'Initializer', init || { list: [] }, ' ')); channels_ts.push(objectType(channelName + 'Initializer', init || { list: [] }, ' '));
addScheme(channelName + 'Initializer', objectTypeScheme(init || { list: [] }, ' '));
let extendsName = 'Channel'; let extendsName = 'Channel';
if (item.words.length === 4 && item.words[2] === 'extends') if (item.words.length === 4 && item.words[2] === 'extends')
extendsName = item.words[3] + 'Channel'; extendsName = item.words[3] + 'Channel';
else if (item.words.length !== 2) else if (item.words.length !== 2)
raise(item); raise(item);
result.push(`export interface ${channelName}Channel extends ${extendsName} {`); channels_ts.push(`export interface ${channelName}Channel extends ${extendsName} {`);
const types = new Map(); const types = new Map();
for (const method of item.list) { for (const method of item.list) {
@ -174,12 +240,20 @@ for (const item of list) {
const parameters = method.list.find(i => i.words[0] === 'parameters'); const parameters = method.list.find(i => i.words[0] === 'parameters');
const paramsName = `${channelName}${titleCase(methodName)}Params`; const paramsName = `${channelName}${titleCase(methodName)}Params`;
types.set(paramsName, parameters || { list: [] }); types.set(paramsName, parameters || { list: [] });
addScheme(paramsName, parameters ? objectTypeScheme(parameters, ' ') : `tOptional(tObject({}))`);
const returns = method.list.find(i => i.words[0] === 'returns'); const returns = method.list.find(i => i.words[0] === 'returns');
const resultName = `${channelName}${titleCase(methodName)}Result`; const resultName = `${channelName}${titleCase(methodName)}Result`;
types.set(resultName, returns); types.set(resultName, returns);
addScheme(resultName, returns ? objectTypeScheme(returns, ' ') : `tUndefined`);
result.push(` ${methodName}(params${parameters ? '' : '?'}: ${paramsName}): Promise<${resultName}>;`); channels_ts.push(` ${methodName}(params${parameters ? '' : '?'}: ${paramsName}): Promise<${resultName}>;`);
for (const key of inherits.keys()) {
if (inherits.get(key) === channelName) {
addScheme(`${key}${titleCase(methodName)}Params`, `tType('${paramsName}')`);
addScheme(`${key}${titleCase(methodName)}Result`, `tType('${resultName}')`);
}
}
} else if (method.words[0] === 'event') { } else if (method.words[0] === 'event') {
if (method.words.length !== 2) if (method.words.length !== 2)
raise(method); raise(method);
@ -188,23 +262,56 @@ for (const item of list) {
const parameters = method.list.find(i => i.words[0] === 'parameters'); const parameters = method.list.find(i => i.words[0] === 'parameters');
const paramsName = `${channelName}${titleCase(eventName)}Event`; const paramsName = `${channelName}${titleCase(eventName)}Event`;
types.set(paramsName, parameters || { list: [] }); types.set(paramsName, parameters || { list: [] });
addScheme(paramsName, objectTypeScheme(parameters || { list: [] }, ' '));
result.push(` on(event: '${eventName}', callback: (params: ${paramsName}) => void): this;`); channels_ts.push(` on(event: '${eventName}', callback: (params: ${paramsName}) => void): this;`);
for (const key of inherits.keys()) {
if (inherits.get(key) === channelName)
addScheme(`${key}${titleCase(eventName)}Event`, `tType('${paramsName}')`);
}
} else { } else {
raise(method); raise(method);
} }
} }
result.push(`}`); channels_ts.push(`}`);
for (const [name, item] of types) { for (const [name, item] of types) {
if (!item) if (!item)
result.push(`export type ${name} = void;`); channels_ts.push(`export type ${name} = void;`);
else else
result.push(objectType(name, item, ' ')); channels_ts.push(objectType(name, item, ' '));
} }
} else { } else {
raise(item); raise(item);
} }
result.push(``); channels_ts.push(``);
} }
fs.writeFileSync(path.join(__dirname, '..', 'src', 'rpc', 'channels.ts'), result.join('\n'), 'utf-8');
const client_validator_ts = [
`/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file is generated by ${path.basename(__filename)}, do not edit manually.
import { scheme, tOptional, tObject, tBoolean, tNumber, tString, tType, tEnum, tArray, tChannel, tUndefined, tBinary } from './validatorPrimitives';
export { validateParams } from './validatorPrimitives';
`];
for (const [name, value] of scheme)
client_validator_ts.push(`scheme.${name} = ${value};`);
client_validator_ts.push(``);
fs.writeFileSync(path.join(__dirname, '..', 'src', 'rpc', 'channels.ts'), channels_ts.join('\n'), 'utf-8');
fs.writeFileSync(path.join(__dirname, '..', 'src', 'rpc', 'client', 'validator.ts'), client_validator_ts.join('\n'), 'utf-8');