chore(dotnet): generate object types from protocol (#14342)
This commit is contained in:
parent
ae37decd73
commit
806b9c8764
|
|
@ -25,141 +25,7 @@ const channels = new Set();
|
||||||
const inherits = new Map();
|
const inherits = new Map();
|
||||||
const mixins = new Map();
|
const mixins = new Map();
|
||||||
|
|
||||||
function raise(item) {
|
const COPYRIGHT_HEADER = `/*
|
||||||
throw new Error('Invalid item: ' + JSON.stringify(item, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function titleCase(name) {
|
|
||||||
return name[0].toUpperCase() + name.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapType(type) {
|
|
||||||
if (type === 'SerializedValue')
|
|
||||||
return 'System.Text.Json.JsonElement';
|
|
||||||
if (type === 'boolean')
|
|
||||||
return 'bool';
|
|
||||||
if (type === 'number')
|
|
||||||
return 'int';
|
|
||||||
// TODO: keep the same names in .NET as upstream
|
|
||||||
if (type === 'ResourceTiming')
|
|
||||||
return 'RequestTimingResult';
|
|
||||||
if (type === 'LifecycleEvent')
|
|
||||||
return 'WaitUntilState';
|
|
||||||
if (type === 'NameValue')
|
|
||||||
return 'HeaderEntry';
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nullableSuffix(inner) {
|
|
||||||
if (['int', 'boolean'].includes(inner.ts))
|
|
||||||
return inner.optional ? '?' : '';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function inlineType(type, indent = '', wrapEnums = false) {
|
|
||||||
if (typeof type === 'string') {
|
|
||||||
const optional = type.endsWith('?');
|
|
||||||
if (optional)
|
|
||||||
type = type.substring(0, type.length - 1);
|
|
||||||
if (type === 'binary')
|
|
||||||
return { ts: 'byte[]', scheme: 'tArray(tByte)', optional };
|
|
||||||
if (type === 'json')
|
|
||||||
return { ts: 'any', scheme: 'tAny', optional };
|
|
||||||
if (['string', 'boolean', 'number', 'undefined'].includes(type)) {
|
|
||||||
return { ts: mapType(type), scheme: `t${titleCase(type)}`, optional };
|
|
||||||
}
|
|
||||||
if (channels.has(type))
|
|
||||||
return { ts: `Core.${type}`, scheme: `tChannel('${type}')` , optional };
|
|
||||||
if (type === 'Channel')
|
|
||||||
return { ts: `Channel`, scheme: `tChannel('*')`, optional };
|
|
||||||
return { ts: mapType(type), scheme: `tType('${type}')`, optional };
|
|
||||||
}
|
|
||||||
if (type.type.startsWith('array')) {
|
|
||||||
const optional = type.type.endsWith('?');
|
|
||||||
const inner = inlineType(type.items, indent, true);
|
|
||||||
return { ts: `List<${inner.ts}>`, scheme: `tArray(${inner.scheme})`, optional };
|
|
||||||
}
|
|
||||||
if (type.type.startsWith('enum')) {
|
|
||||||
if (type.literals.includes('networkidle'))
|
|
||||||
return { ts: 'LoadState', scheme: `tString`, optional: false };
|
|
||||||
return { ts: 'string', scheme: `tString`, optional: false };
|
|
||||||
}
|
|
||||||
if (type.type.startsWith('object')) {
|
|
||||||
const optional = type.type.endsWith('?');
|
|
||||||
const custom = processCustomType(type, optional);
|
|
||||||
if (custom)
|
|
||||||
return custom;
|
|
||||||
const inner = properties(type.properties, indent + ' ');
|
|
||||||
return {
|
|
||||||
ts: `{\n${inner.ts}\n${indent}}`,
|
|
||||||
scheme: `tObject({\n${inner.scheme}\n${indent}})`,
|
|
||||||
optional
|
|
||||||
};
|
|
||||||
}
|
|
||||||
raise(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function properties(properties, indent, onlyOptional) {
|
|
||||||
const ts = [];
|
|
||||||
const scheme = [];
|
|
||||||
const visitProperties = props => {
|
|
||||||
for (const [name, value] of Object.entries(props)) {
|
|
||||||
if (name === 'android' || name === 'electron')
|
|
||||||
continue;
|
|
||||||
if (name.startsWith('$mixin')) {
|
|
||||||
visitProperties(mixins.get(value).properties);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const inner = inlineType(value, indent);
|
|
||||||
if (onlyOptional && !inner.optional)
|
|
||||||
continue;
|
|
||||||
ts.push('');
|
|
||||||
ts.push(`${indent}[JsonPropertyName("${name}")]`)
|
|
||||||
ts.push(`${indent}public ${inner.ts}${nullableSuffix(inner)} ${toTitleCase(name)} { get; set; }`);
|
|
||||||
const wrapped = inner.optional ? `tOptional(${inner.scheme})` : inner.scheme;
|
|
||||||
scheme.push(`${indent}${name}: ${wrapped},`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
visitProperties(properties);
|
|
||||||
return { ts: ts.join('\n'), scheme: scheme.join('\n') };
|
|
||||||
}
|
|
||||||
|
|
||||||
function objectType(props, indent, onlyOptional = false) {
|
|
||||||
if (!Object.entries(props).length)
|
|
||||||
return { ts: `${indent}{\n${indent}}`, scheme: `tObject({})` };
|
|
||||||
const inner = properties(props, indent + ' ', onlyOptional);
|
|
||||||
return { ts: `${indent}{${inner.ts}\n${indent}}`, scheme: `tObject({\n${inner.scheme}\n${indent}})` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const yml = fs.readFileSync(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'protocol.yml'), 'utf-8');
|
|
||||||
const protocol = yaml.parse(yml);
|
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(protocol)) {
|
|
||||||
if (value.type === 'interface') {
|
|
||||||
channels.add(name);
|
|
||||||
if (value.extends)
|
|
||||||
inherits.set(name, value.extends);
|
|
||||||
}
|
|
||||||
if (value.type === 'mixin')
|
|
||||||
mixins.set(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.argv[2]) {
|
|
||||||
console.error('.NET repository needs to be specified as an argument.\n'+ `Usage: node ${path.relative(process.cwd(), __filename)} ../playwright-dotnet/src/Playwright/`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dir = path.join(process.argv[2], 'Transport', 'Protocol', 'Generated')
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
|
|
||||||
for (const [name, item] of Object.entries(protocol)) {
|
|
||||||
if (item.type === 'interface') {
|
|
||||||
const channelName = name;
|
|
||||||
const channels_ts = [];
|
|
||||||
const init = objectType(item.initializer || {}, ' ');
|
|
||||||
const initializerName = channelName + 'Initializer';
|
|
||||||
const superName = inherits.get(name);
|
|
||||||
channels_ts.push(`/*
|
|
||||||
* MIT License
|
* MIT License
|
||||||
*
|
*
|
||||||
* Copyright (c) Microsoft Corporation.
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
|
@ -182,18 +48,174 @@ for (const [name, item] of Object.entries(protocol)) {
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
`)
|
`;
|
||||||
|
|
||||||
|
function raise(item) {
|
||||||
|
throw new Error('Invalid item: ' + JSON.stringify(item, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function titleCase(name) {
|
||||||
|
return name[0].toUpperCase() + name.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapType(type) {
|
||||||
|
if (type === 'SerializedValue')
|
||||||
|
return 'System.Text.Json.JsonElement';
|
||||||
|
if (type === 'boolean')
|
||||||
|
return 'bool';
|
||||||
|
if (type === 'number')
|
||||||
|
return 'int';
|
||||||
|
// TODO: keep the same names in .NET as upstream
|
||||||
|
if (type === 'ResourceTiming')
|
||||||
|
return 'RequestTimingResult';
|
||||||
|
if (type === 'LifecycleEvent')
|
||||||
|
return 'WaitUntilState';
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nullableSuffix(inner) {
|
||||||
|
if (['int', 'boolean'].includes(inner.ts))
|
||||||
|
return inner.optional ? '?' : '';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function inlineType(type, indent = '', name, level) {
|
||||||
|
if (typeof type === 'string') {
|
||||||
|
const optional = type.endsWith('?');
|
||||||
|
if (optional)
|
||||||
|
type = type.substring(0, type.length - 1);
|
||||||
|
if (type === 'binary')
|
||||||
|
return { ts: 'byte[]', scheme: 'tArray(tByte)', optional };
|
||||||
|
if (type === 'json')
|
||||||
|
return { ts: 'any', scheme: 'tAny', optional };
|
||||||
|
if (['string', 'boolean', 'number', 'undefined'].includes(type))
|
||||||
|
return { ts: mapType(type), scheme: `t${titleCase(type)}`, optional };
|
||||||
|
if (channels.has(type))
|
||||||
|
return { ts: `Core.${type}`, scheme: `tChannel('${type}')` , optional };
|
||||||
|
if (type === 'Channel')
|
||||||
|
return { ts: `Channel`, scheme: `tChannel('*')`, optional };
|
||||||
|
return { ts: mapType(type), scheme: `tType('${type}')`, optional };
|
||||||
|
}
|
||||||
|
if (type.type.startsWith('array')) {
|
||||||
|
const optional = type.type.endsWith('?');
|
||||||
|
const inner = inlineType(type.items, indent, name, level);
|
||||||
|
return { ts: `List<${inner.ts}>`, scheme: `tArray(${inner.scheme})`, optional };
|
||||||
|
}
|
||||||
|
if (type.type.startsWith('enum')) {
|
||||||
|
if (type.literals.includes('networkidle'))
|
||||||
|
return { ts: 'LoadState', scheme: `tString`, optional: false };
|
||||||
|
return { ts: 'string', scheme: `tString`, optional: false };
|
||||||
|
}
|
||||||
|
if (type.type.startsWith('object')) {
|
||||||
|
const optional = type.type.endsWith('?');
|
||||||
|
|
||||||
|
const custom = processCustomType(type, optional);
|
||||||
|
if (custom)
|
||||||
|
return custom;
|
||||||
|
if (level >= 1) {
|
||||||
|
const inner = properties(type.properties, ' ', false, name, level);
|
||||||
|
writeCSharpClass(name, null, ' {' + inner.ts + '\n }');
|
||||||
|
return { ts: name, scheme: 'tObject()', optional };
|
||||||
|
}
|
||||||
|
|
||||||
|
const inner = properties(type.properties, indent + ' ', false, name, level);
|
||||||
|
return {
|
||||||
|
ts: `{\n${inner.ts}\n${indent}}`,
|
||||||
|
scheme: `tObject({\n${inner.scheme}\n${indent}})`,
|
||||||
|
optional
|
||||||
|
};
|
||||||
|
}
|
||||||
|
raise(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function properties(properties, indent, onlyOptional, parentName, level) {
|
||||||
|
const ts = [];
|
||||||
|
const scheme = [];
|
||||||
|
const visitProperties = (props, parentName) => {
|
||||||
|
for (const [name, value] of Object.entries(props)) {
|
||||||
|
if (name === 'android' || name === 'electron')
|
||||||
|
continue;
|
||||||
|
if (name.startsWith('$mixin')) {
|
||||||
|
visitProperties(mixins.get(value).properties, parentName + toTitleCase(name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const inner = inlineType(value, indent, parentName + toTitleCase(name), level + 1);
|
||||||
|
if (onlyOptional && !inner.optional)
|
||||||
|
continue;
|
||||||
|
ts.push('');
|
||||||
|
ts.push(`${indent}[JsonPropertyName("${name}")]`);
|
||||||
|
ts.push(`${indent}public ${inner.ts}${nullableSuffix(inner)} ${toTitleCase(name)} { get; set; }`);
|
||||||
|
const wrapped = inner.optional ? `tOptional(${inner.scheme})` : inner.scheme;
|
||||||
|
scheme.push(`${indent}${name}: ${wrapped},`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visitProperties(properties, parentName);
|
||||||
|
return { ts: ts.join('\n'), scheme: scheme.join('\n') };
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectType(props, indent, onlyOptional = false, parentName = '') {
|
||||||
|
if (!Object.entries(props).length)
|
||||||
|
return { ts: `${indent}{\n${indent}}`, scheme: `tObject({})` };
|
||||||
|
const inner = properties(props, indent + ' ', onlyOptional, parentName, 0);
|
||||||
|
return { ts: `${indent}{${inner.ts}\n${indent}}`, scheme: `tObject({\n${inner.scheme}\n${indent}})` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const yml = fs.readFileSync(path.join(__dirname, '..', 'packages', 'playwright-core', 'src', 'protocol', 'protocol.yml'), 'utf-8');
|
||||||
|
const protocol = yaml.parse(yml);
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(protocol)) {
|
||||||
|
if (value.type === 'interface') {
|
||||||
|
channels.add(name);
|
||||||
|
if (value.extends)
|
||||||
|
inherits.set(name, value.extends);
|
||||||
|
}
|
||||||
|
if (value.type === 'mixin')
|
||||||
|
mixins.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.argv[2]) {
|
||||||
|
console.error('.NET repository needs to be specified as an argument.\n' + `Usage: node ${path.relative(process.cwd(), __filename)} ../playwright-dotnet/src/Playwright/`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = path.join(process.argv[2], 'Transport', 'Protocol', 'Generated');
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
for (const [name, item] of Object.entries(protocol)) {
|
||||||
|
if (item.type === 'interface') {
|
||||||
|
const init = objectType(item.initializer || {}, ' ');
|
||||||
|
const initializerName = name + 'Initializer';
|
||||||
|
const superName = inherits.has(name) ? inherits.get(name) + 'Initializer' : null;
|
||||||
|
writeCSharpClass(initializerName, superName, init.ts);
|
||||||
|
} else if (item.type === 'object') {
|
||||||
|
if (Object.keys(item.properties).length === 0)
|
||||||
|
continue;
|
||||||
|
const init = objectType(item.properties, ' ', false, name);
|
||||||
|
writeCSharpClass(name, null, init.ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} className
|
||||||
|
* @param {string|undefined} inheritFrom
|
||||||
|
* @param {any} serializedProperties
|
||||||
|
*/
|
||||||
|
function writeCSharpClass(className, inheritFrom, serializedProperties) {
|
||||||
|
if (className === 'SerializedArgument')
|
||||||
|
return;
|
||||||
|
const channels_ts = [];
|
||||||
|
channels_ts.push(COPYRIGHT_HEADER);
|
||||||
channels_ts.push('using System.Collections.Generic;');
|
channels_ts.push('using System.Collections.Generic;');
|
||||||
channels_ts.push('using System.Text.Json.Serialization;')
|
channels_ts.push('using System.Text.Json.Serialization;');
|
||||||
channels_ts.push(``);
|
channels_ts.push(``);
|
||||||
channels_ts.push(`namespace Microsoft.Playwright.Transport.Protocol`);
|
channels_ts.push(`namespace Microsoft.Playwright.Transport.Protocol`);
|
||||||
channels_ts.push(`{`);
|
channels_ts.push(`{`);
|
||||||
channels_ts.push(` internal class ${initializerName}${superName ? ' : ' + superName + 'Initializer' : ''}`);
|
channels_ts.push(` internal class ${className}${inheritFrom ? ' : ' + inheritFrom : ''}`);
|
||||||
channels_ts.push(init.ts);
|
channels_ts.push(serializedProperties);
|
||||||
channels_ts.push(`}`);
|
channels_ts.push(`}`);
|
||||||
channels_ts.push(``);
|
channels_ts.push(``);
|
||||||
writeFile(`${initializerName}.cs`, channels_ts.join('\n'));
|
writeFile(`${className}.cs`, channels_ts.join('\n'));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeFile(file, content) {
|
function writeFile(file, content) {
|
||||||
|
|
@ -212,24 +234,24 @@ function processCustomType(type, optional) {
|
||||||
if (type.properties.name
|
if (type.properties.name
|
||||||
&& type.properties.value
|
&& type.properties.value
|
||||||
&& inlineType(type.properties.name).ts === 'string'
|
&& inlineType(type.properties.name).ts === 'string'
|
||||||
&& inlineType(type.properties.value).ts === 'string') {
|
&& inlineType(type.properties.value).ts === 'string')
|
||||||
return { ts: 'HeaderEntry', scheme: 'tObject()', optional };
|
return { ts: 'HeaderEntry', scheme: 'tObject()', optional };
|
||||||
}
|
|
||||||
if (type.properties.width
|
if (type.properties.width
|
||||||
&& type.properties.height
|
&& type.properties.height
|
||||||
&& inlineType(type.properties.width).ts === 'int'
|
&& inlineType(type.properties.width).ts === 'int'
|
||||||
&& inlineType(type.properties.height).ts === 'int') {
|
&& inlineType(type.properties.height).ts === 'int')
|
||||||
return { ts: 'ViewportSize', scheme: 'tObject()', optional };
|
return { ts: 'ViewportSize', scheme: 'tObject()', optional };
|
||||||
}
|
|
||||||
if (type.properties.url
|
if (type.properties.url
|
||||||
&& type.properties.lineNumber
|
&& type.properties.lineNumber
|
||||||
&& inlineType(type.properties.url).ts === 'string'
|
&& inlineType(type.properties.url).ts === 'string'
|
||||||
&& inlineType(type.properties.lineNumber).ts === 'int') {
|
&& inlineType(type.properties.lineNumber).ts === 'int')
|
||||||
return { ts: 'ConsoleMessageLocation', scheme: 'tObject()', optional };
|
return { ts: 'ConsoleMessageLocation', scheme: 'tObject()', optional };
|
||||||
}
|
|
||||||
if (type.properties.name
|
if (type.properties.name
|
||||||
&& type.properties.descriptor
|
&& type.properties.descriptor
|
||||||
&& inlineType(type.properties.name).ts === 'string') {
|
&& inlineType(type.properties.name).ts === 'string')
|
||||||
return { ts: 'DeviceDescriptorEntry', scheme: 'tObject()', optional };
|
return { ts: 'DeviceDescriptorEntry', scheme: 'tObject()', optional };
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue