feat(rpc): client-side parameters validation (#3069)
This commit is contained in:
parent
e56e148597
commit
b1a5a02154
|
|
@ -387,9 +387,9 @@ export class Page extends EventEmitter {
|
|||
|
||||
async emulateMedia(options: { media?: types.MediaType | null, colorScheme?: types.ColorScheme | null }) {
|
||||
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)
|
||||
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)
|
||||
this._state.mediaType = options.media;
|
||||
if (options.colorScheme !== undefined)
|
||||
|
|
|
|||
|
|
@ -620,8 +620,8 @@ export type PageCloseParams = {
|
|||
};
|
||||
export type PageCloseResult = void;
|
||||
export type PageEmulateMediaParams = {
|
||||
media?: 'screen' | 'print' | 'reset',
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference' | 'reset',
|
||||
media?: 'screen' | 'print' | 'null',
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference' | 'null',
|
||||
};
|
||||
export type PageEmulateMediaResult = void;
|
||||
export type PageExposeBindingParams = {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { BrowserServer } from './browserServer';
|
|||
import { LoggerSink } from '../../loggerSink';
|
||||
import { headersObjectToArray, envObjectToArray } from '../serializers';
|
||||
import { serializeArgument } from './jsHandle';
|
||||
import { assert } from '../../helper';
|
||||
|
||||
type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
|
||||
|
||||
|
|
@ -48,6 +49,8 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
|
|||
const logger = options.logger;
|
||||
options = { ...options, logger: undefined };
|
||||
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 = {
|
||||
...options,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}
|
|||
return obj.addListener;
|
||||
if (prop === 'removeEventListener')
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { ChromiumBrowser } from './chromiumBrowser';
|
|||
import { ChromiumBrowserContext } from './chromiumBrowserContext';
|
||||
import { Selectors } from './selectors';
|
||||
import { Stream } from './stream';
|
||||
import { validateParams } from './validator';
|
||||
|
||||
class Root extends ChannelOwner<Channel, {}> {
|
||||
constructor(connection: Connection) {
|
||||
|
|
@ -63,9 +64,10 @@ export class Connection {
|
|||
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 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);
|
||||
this.onmessage(converted);
|
||||
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));
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!payload)
|
||||
return payload;
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
await this._elementChannel.selectText(options);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { assertMaxArguments, helper } from '../../helper';
|
||||
import { assertMaxArguments, helper, assert } from '../../helper';
|
||||
import * as types from '../../types';
|
||||
import { FrameChannel, FrameInitializer, FrameNavigatedEvent } from '../channels';
|
||||
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> {
|
||||
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> {
|
||||
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 });
|
||||
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> {
|
||||
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> {
|
||||
return this._wrapApiCall(this._apiName('addStyleTag'), async () => {
|
||||
const copy = { ...options };
|
||||
if (copy.path)
|
||||
if (copy.path) {
|
||||
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, Arg>(pageFunction: Func1<Arg, R>, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise<SmartHandle<R>> {
|
||||
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({
|
||||
...options,
|
||||
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')
|
||||
waitUntil = 'networkidle';
|
||||
if (!types.kLifecycleEvents.has(waitUntil))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { Dialog } from './dialog';
|
|||
import { Download } from './download';
|
||||
import { ElementHandle } from './elementHandle';
|
||||
import { Worker } from './worker';
|
||||
import { Frame, FunctionWithSource, GotoOptions } from './frame';
|
||||
import { Frame, FunctionWithSource, GotoOptions, verifyLoadState } from './frame';
|
||||
import { Keyboard, Mouse } from './input';
|
||||
import { Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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 }) {
|
||||
return this._wrapApiCall('page.emulateMedia', async () => {
|
||||
await this._channel.emulateMedia({
|
||||
media: options.media === null ? 'reset' : options.media,
|
||||
colorScheme: options.colorScheme === null ? 'reset' : options.colorScheme,
|
||||
media: options.media === null ? 'null' : options.media,
|
||||
colorScheme: options.colorScheme === null ? 'null' : options.colorScheme,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
1391
src/rpc/client/validator.ts
Normal file
1391
src/rpc/client/validator.ts
Normal file
File diff suppressed because it is too large
Load diff
123
src/rpc/client/validatorPrimitives.ts
Normal file
123
src/rpc/client/validatorPrimitives.ts
Normal 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 } = {};
|
||||
|
|
@ -530,13 +530,13 @@ interface Page
|
|||
screen
|
||||
print
|
||||
# Reset emulated value to the system default.
|
||||
reset
|
||||
null
|
||||
colorScheme?: enum
|
||||
dark
|
||||
light
|
||||
no-preference
|
||||
# Reset emulated value to the system default.
|
||||
reset
|
||||
null
|
||||
|
||||
command exposeBinding
|
||||
parameters
|
||||
|
|
|
|||
|
|
@ -108,8 +108,8 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
|
|||
|
||||
async emulateMedia(params: PageEmulateMediaParams): Promise<void> {
|
||||
await this._page.emulateMedia({
|
||||
media: params.media === 'reset' ? null : params.media,
|
||||
colorScheme: params.colorScheme === 'reset' ? null : params.colorScheme,
|
||||
media: params.media === 'null' ? null : params.media,
|
||||
colorScheme: params.colorScheme === 'null' ? null : params.colorScheme,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ describe('Page.emulateMedia type', function() {
|
|||
it('should throw in case of bad type argument', async({page, server}) => {
|
||||
let error = null;
|
||||
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}) => {
|
||||
let error = null;
|
||||
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}) => {
|
||||
await page.emulateMedia({ colorScheme: 'light' });
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ const { Connection } = require('../../lib/rpc/client/connection');
|
|||
const { Transport } = require('../../lib/rpc/transport');
|
||||
const { PlaywrightDispatcher } = require('../../lib/rpc/server/playwrightDispatcher');
|
||||
const { setUseApiName } = require('../../lib/progress');
|
||||
const { setUnderTest } = require('../../lib/helper');
|
||||
setUnderTest();
|
||||
|
||||
const browserName = process.env.BROWSER || 'chromium';
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ describe('Playwright', function() {
|
|||
const error = await browserType.launch(options).catch(e => e);
|
||||
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 });
|
||||
await browser.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,8 +72,6 @@ function inlineType(item, indent) {
|
|||
inner = `{\n${properties(item, indent + ' ')}\n${indent}}`;
|
||||
} else if (type === 'binary') {
|
||||
inner = 'Binary';
|
||||
} else if (type === 'Error') {
|
||||
inner = 'SerializedError';
|
||||
} else if (channels.has(type)) {
|
||||
inner = type + 'Channel';
|
||||
} else {
|
||||
|
|
@ -82,6 +80,35 @@ function inlineType(item, indent) {
|
|||
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) {
|
||||
const result = [];
|
||||
for (const prop of item.list) {
|
||||
|
|
@ -99,13 +126,39 @@ function properties(item, indent) {
|
|||
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) {
|
||||
if (!item.list.length)
|
||||
return `export type ${name} = {};`;
|
||||
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.
|
||||
*
|
||||
|
|
@ -134,33 +187,46 @@ export interface Channel extends EventEmitter {
|
|||
|
||||
const pdl = fs.readFileSync(path.join(__dirname, '..', 'src', 'rpc', 'protocol.pdl'), 'utf-8');
|
||||
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) {
|
||||
if (item.words[0] === 'interface')
|
||||
if (item.words[0] === 'interface') {
|
||||
channels.add(item.words[1]);
|
||||
if (item.words[2] === 'extends')
|
||||
inherits.set(item.words[1], item.words[3]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of list) {
|
||||
if (item.words[0] === 'type') {
|
||||
if (item.words.length !== 2)
|
||||
raise(item);
|
||||
result.push(`export type ${item.words[1]} = {`);
|
||||
result.push(properties(item, ' '));
|
||||
result.push(`};`);
|
||||
channels_ts.push(`export type ${item.words[1]} = {`);
|
||||
channels_ts.push(properties(item, ' '));
|
||||
channels_ts.push(`};`);
|
||||
addScheme(item.words[1], objectTypeScheme(item, ' '));
|
||||
} else if (item.words[0] === 'interface') {
|
||||
const channelName = item.words[1];
|
||||
result.push(`// ----------- ${channelName} -----------`);
|
||||
channels_ts.push(`// ----------- ${channelName} -----------`);
|
||||
const init = item.list.find(i => i.words[0] === 'initializer');
|
||||
if (init && init.words.length > 1)
|
||||
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';
|
||||
if (item.words.length === 4 && item.words[2] === 'extends')
|
||||
extendsName = item.words[3] + 'Channel';
|
||||
else if (item.words.length !== 2)
|
||||
raise(item);
|
||||
result.push(`export interface ${channelName}Channel extends ${extendsName} {`);
|
||||
channels_ts.push(`export interface ${channelName}Channel extends ${extendsName} {`);
|
||||
|
||||
const types = new Map();
|
||||
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 paramsName = `${channelName}${titleCase(methodName)}Params`;
|
||||
types.set(paramsName, parameters || { list: [] });
|
||||
addScheme(paramsName, parameters ? objectTypeScheme(parameters, ' ') : `tOptional(tObject({}))`);
|
||||
|
||||
const returns = method.list.find(i => i.words[0] === 'returns');
|
||||
const resultName = `${channelName}${titleCase(methodName)}Result`;
|
||||
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') {
|
||||
if (method.words.length !== 2)
|
||||
raise(method);
|
||||
|
|
@ -188,23 +262,56 @@ for (const item of list) {
|
|||
const parameters = method.list.find(i => i.words[0] === 'parameters');
|
||||
const paramsName = `${channelName}${titleCase(eventName)}Event`;
|
||||
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 {
|
||||
raise(method);
|
||||
}
|
||||
}
|
||||
result.push(`}`);
|
||||
channels_ts.push(`}`);
|
||||
for (const [name, item] of types) {
|
||||
if (!item)
|
||||
result.push(`export type ${name} = void;`);
|
||||
channels_ts.push(`export type ${name} = void;`);
|
||||
else
|
||||
result.push(objectType(name, item, ' '));
|
||||
channels_ts.push(objectType(name, item, ' '));
|
||||
}
|
||||
} else {
|
||||
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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue