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 mixins = new Map();
|
||||
|
||||
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';
|
||||
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(`/*
|
||||
const COPYRIGHT_HEADER = `/*
|
||||
* MIT License
|
||||
*
|
||||
* 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
|
||||
* 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.Text.Json.Serialization;')
|
||||
channels_ts.push('using System.Text.Json.Serialization;');
|
||||
channels_ts.push(``);
|
||||
channels_ts.push(`namespace Microsoft.Playwright.Transport.Protocol`);
|
||||
channels_ts.push(`{`);
|
||||
channels_ts.push(` internal class ${initializerName}${superName ? ' : ' + superName + 'Initializer' : ''}`);
|
||||
channels_ts.push(init.ts);
|
||||
channels_ts.push(` internal class ${className}${inheritFrom ? ' : ' + inheritFrom : ''}`);
|
||||
channels_ts.push(serializedProperties);
|
||||
channels_ts.push(`}`);
|
||||
channels_ts.push(``);
|
||||
writeFile(`${initializerName}.cs`, channels_ts.join('\n'));
|
||||
}
|
||||
writeFile(`${className}.cs`, channels_ts.join('\n'));
|
||||
}
|
||||
|
||||
function writeFile(file, content) {
|
||||
|
|
@ -212,24 +234,24 @@ function processCustomType(type, optional) {
|
|||
if (type.properties.name
|
||||
&& type.properties.value
|
||||
&& inlineType(type.properties.name).ts === 'string'
|
||||
&& inlineType(type.properties.value).ts === 'string') {
|
||||
&& inlineType(type.properties.value).ts === 'string')
|
||||
return { ts: 'HeaderEntry', scheme: 'tObject()', optional };
|
||||
}
|
||||
|
||||
if (type.properties.width
|
||||
&& type.properties.height
|
||||
&& inlineType(type.properties.width).ts === 'int'
|
||||
&& inlineType(type.properties.height).ts === 'int') {
|
||||
&& inlineType(type.properties.height).ts === 'int')
|
||||
return { ts: 'ViewportSize', scheme: 'tObject()', optional };
|
||||
}
|
||||
|
||||
if (type.properties.url
|
||||
&& type.properties.lineNumber
|
||||
&& inlineType(type.properties.url).ts === 'string'
|
||||
&& inlineType(type.properties.lineNumber).ts === 'int') {
|
||||
&& inlineType(type.properties.lineNumber).ts === 'int')
|
||||
return { ts: 'ConsoleMessageLocation', scheme: 'tObject()', optional };
|
||||
}
|
||||
|
||||
if (type.properties.name
|
||||
&& type.properties.descriptor
|
||||
&& inlineType(type.properties.name).ts === 'string') {
|
||||
&& inlineType(type.properties.name).ts === 'string')
|
||||
return { ts: 'DeviceDescriptorEntry', scheme: 'tObject()', optional };
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue