mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			915 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			915 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * 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.
 | |
|  */
 | |
| 
 | |
| // @ts-check
 | |
| 
 | |
| const path = require('path');
 | |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | |
| const Documentation = require('./documentation');
 | |
| const XmlDoc = require('./dotnetXmlDocumentation');
 | |
| const PROJECT_DIR = path.join(__dirname, '..', '..');
 | |
| const fs = require('fs');
 | |
| const { parseApi } = require('./api_parser');
 | |
| const { Type } = require('./documentation');
 | |
| const { EOL } = require('os');
 | |
| const { execSync } = require('child_process');
 | |
| 
 | |
| const maxDocumentationColumnWidth = 80;
 | |
| Error.stackTraceLimit = 100;
 | |
| 
 | |
| /** @type {Map<string, Documentation.Type>} */
 | |
| const modelTypes = new Map(); // this will hold types that we discover, because of .NET specifics, like results
 | |
| /** @type {Map<string, string>} */
 | |
| const documentedResults = new Map(); // will hold documentation for new types
 | |
| /** @type {Map<string, string[]>} */
 | |
| const enumTypes = new Map();
 | |
| /** @type {Map<string, Documentation.Type>} */
 | |
| const optionTypes = new Map();
 | |
| const customTypeNames = new Map([
 | |
|   ['domcontentloaded', 'DOMContentLoaded'],
 | |
|   ['networkidle', 'NetworkIdle'],
 | |
| ]);
 | |
| 
 | |
| const outputDir = process.argv[2] || path.join(__dirname, 'generate_types', 'csharp');
 | |
| const apiDir = path.join(outputDir, 'API', 'Generated');
 | |
| const optionsDir = path.join(outputDir, 'API', 'Generated', 'Options');
 | |
| const enumsDir = path.join(outputDir, 'API', 'Generated', 'Enums');
 | |
| const typesDir = path.join(outputDir, 'API', 'Generated', 'Types');
 | |
| 
 | |
| for (const dir of [apiDir, optionsDir, enumsDir, typesDir])
 | |
|   fs.mkdirSync(dir, { recursive: true });
 | |
| 
 | |
| const documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
 | |
| documentation.filterForLanguage('csharp');
 | |
| 
 | |
| documentation.setLinkRenderer(item => {
 | |
|   const asyncSuffix = item.member && item.member.async ? 'Async' : '';
 | |
|   if (item.clazz)
 | |
|     return `<see cref="I${toTitleCase(item.clazz.name)}"/>`;
 | |
|   else if (item.member)
 | |
|     return `<see cref="I${toTitleCase(item.member.clazz.name)}.${toMemberName(item.member)}${asyncSuffix}"/>`;
 | |
|   else if (item.option)
 | |
|     return `<paramref name="${item.option}"/>`;
 | |
|   else if (item.param)
 | |
|     return `<paramref name="${item.param}"/>`;
 | |
|   else
 | |
|     throw new Error('Unknown link format.');
 | |
| });
 | |
| 
 | |
| // get the template for a class
 | |
| const template = fs.readFileSync(path.join(__dirname, 'templates', 'interface.cs'), 'utf-8');
 | |
| 
 | |
| // map the name to a C# friendly one (we prepend an I to denote an interface)
 | |
| const classNameMap = new Map(documentation.classesArray.map(x => [x.name, `I${toTitleCase(x.name)}`]));
 | |
| 
 | |
| // map some types that we know of
 | |
| classNameMap.set('Error', 'Exception');
 | |
| classNameMap.set('TimeoutError', 'TimeoutException');
 | |
| classNameMap.set('EvaluationArgument', 'object');
 | |
| classNameMap.set('boolean', 'bool');
 | |
| classNameMap.set('any', 'object');
 | |
| classNameMap.set('Buffer', 'byte[]');
 | |
| classNameMap.set('path', 'string');
 | |
| classNameMap.set('URL', 'string');
 | |
| classNameMap.set('RegExp', 'Regex');
 | |
| classNameMap.set('Readable', 'Stream');
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * @param {string} kind
 | |
|  * @param {string} name
 | |
|  * @param {Documentation.MarkdownNode[]} spec
 | |
|  * @param {string[]} body
 | |
|  * @param {string} folder
 | |
|  * @param {string} extendsName
 | |
|  */
 | |
| function writeFile(kind, name, spec, body, folder, extendsName = null) {
 | |
|   const out = [];
 | |
|   // console.log(`Generating ${name}`);
 | |
| 
 | |
|   if (spec) {
 | |
|     out.push(...XmlDoc.renderXmlDoc(spec, maxDocumentationColumnWidth));
 | |
|   } else {
 | |
|     const ownDocumentation = documentedResults.get(name);
 | |
|     if (ownDocumentation) {
 | |
|       out.push('/// <summary>');
 | |
|       out.push(`/// ${ownDocumentation}`);
 | |
|       out.push('/// </summary>');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (extendsName === 'IEventEmitter')
 | |
|     extendsName = null;
 | |
| 
 | |
|   if (body[0] === '')
 | |
|     body = body.slice(1);
 | |
| 
 | |
|   out.push(`${kind} ${name}${extendsName ? ` : ${extendsName}` : ''}`);
 | |
|   out.push('{');
 | |
|   out.push(...body);
 | |
|   out.push('}');
 | |
| 
 | |
|   const content = template.replace('[CONTENT]', out.join(EOL));
 | |
|   fs.writeFileSync(path.join(folder, name + '.cs'), content);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {Documentation.Class} clazz
 | |
|  */
 | |
| function renderClass(clazz) {
 | |
|   const name = classNameMap.get(clazz.name);
 | |
|   if (name === 'TimeoutException')
 | |
|     return;
 | |
| 
 | |
|   const body = [];
 | |
|   for (const member of clazz.membersArray) {
 | |
|     // Classes inherit it from IAsyncDisposable
 | |
|     if (member.name === 'dispose')
 | |
|       continue;
 | |
|     if (member.alias.startsWith('RunAnd'))
 | |
|       renderMember(member, clazz, { trimRunAndPrefix: true }, body);
 | |
|     renderMember(member, clazz, {}, body);
 | |
|   }
 | |
| 
 | |
|   writeFile(
 | |
|       'public partial interface',
 | |
|       name,
 | |
|       clazz.spec,
 | |
|       body,
 | |
|       apiDir,
 | |
|       clazz.extends ? `I${toTitleCase(clazz.extends)}` : null);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} name
 | |
|  * @param {Documentation.Type} type
 | |
|  */
 | |
| function renderModelType(name, type) {
 | |
|   const body = [];
 | |
|   // TODO: consider how this could be merged with the `translateType` check
 | |
|   if (type.union
 | |
|     && type.union[0].name === 'null'
 | |
|     && type.union.length === 2)
 | |
|     type = type.union[1];
 | |
| 
 | |
| 
 | |
|   if (type.name === 'Array') {
 | |
|     throw new Error('Array at this stage is unexpected.');
 | |
|   } else if (type.properties) {
 | |
|     for (const member of type.properties) {
 | |
|       const fakeType = new Type(name, null);
 | |
|       renderMember(member, fakeType, {}, body);
 | |
|     }
 | |
|   } else {
 | |
|     console.log(type);
 | |
|     throw new Error(`Not sure what to do in this case.`);
 | |
|   }
 | |
|   writeFile('public partial class', name, null, body, typesDir);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} name
 | |
|  * @param {string[]} literals
 | |
|  */
 | |
| function renderEnum(name, literals) {
 | |
|   const body = [];
 | |
|   for (let literal of literals) {
 | |
|     // strip out the quotes
 | |
|     literal = literal.replace(/[\"]/g, ``);
 | |
|     const escapedName = literal.replace(/[-]/g, ' ')
 | |
|         .split(' ')
 | |
|         .map(word => customTypeNames.get(word) || word[0].toUpperCase() + word.substring(1)).join('');
 | |
| 
 | |
|     body.push(`[EnumMember(Value = "${literal}")]`);
 | |
|     body.push(`${escapedName},`);
 | |
|   }
 | |
|   writeFile('public enum', name, null, body, enumsDir);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} name
 | |
|  * @param {Documentation.Type} type
 | |
|  */
 | |
| function renderOptionType(name, type) {
 | |
|   const body = [];
 | |
| 
 | |
|   renderConstructors(name, type, body);
 | |
| 
 | |
|   for (const member of type.properties)
 | |
|     renderMember(member, member.type, {}, body);
 | |
|   writeFile('public class', name, null, body, optionsDir);
 | |
| }
 | |
| 
 | |
| for (const element of documentation.classesArray)
 | |
|   renderClass(element);
 | |
| 
 | |
| 
 | |
| for (const [name, type] of optionTypes)
 | |
|   renderOptionType(name, type);
 | |
| 
 | |
| for (const [name, type] of modelTypes)
 | |
|   renderModelType(name, type);
 | |
| 
 | |
| for (const [name, literals] of enumTypes)
 | |
|   renderEnum(name, literals);
 | |
| 
 | |
| /**
 | |
|  * @param {string} name
 | |
|  */
 | |
| function toArgumentName(name) {
 | |
|   return name === 'event' ? `@${name}` : name;
 | |
| }
 | |
| 
 | |
| /**
 | |
| * @param {Documentation.Member} member
 | |
| */
 | |
| function toMemberName(member, makeAsync = false) {
 | |
|   const assumedName = toTitleCase(member.alias || member.name);
 | |
|   if (member.kind === 'interface')
 | |
|     return `I${assumedName}`;
 | |
|   if (makeAsync && member.async)
 | |
|     return assumedName + 'Async';
 | |
|   if (!makeAsync && assumedName.endsWith('Async'))
 | |
|     return assumedName.substring(0, assumedName.length - 'Async'.length);
 | |
|   return assumedName;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} name
 | |
|  * @returns {string}
 | |
|  */
 | |
| function toTitleCase(name) {
 | |
|   return name.charAt(0).toUpperCase() + name.substring(1);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * @param {string} name
 | |
|  * @param {Documentation.Type} type
 | |
|  * @param {string[]} out
 | |
|  */
 | |
| function renderConstructors(name, type, out) {
 | |
|   out.push(`public ${name}(){}`);
 | |
|   out.push('');
 | |
|   out.push(`public ${name}(${name} clone) {`);
 | |
|   out.push(`if(clone == null) return;`);
 | |
| 
 | |
|   type.properties.forEach(p => {
 | |
|     const propType = translateType(p.type, type, t => generateNameDefault(p, name, t, type));
 | |
|     const propName = toMemberName(p);
 | |
|     const overloads = getPropertyOverloads(propType, p, propName, p.type);
 | |
|     for (const { name } of overloads)
 | |
|       out.push(`${name} = clone.${name};`);
 | |
|   });
 | |
|   out.push(`}`);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * @param {Documentation.Member} member
 | |
|  * @param {Documentation.Class|Documentation.Type} parent
 | |
|  * @param {{nojson?: boolean, trimRunAndPrefix?: boolean}} options
 | |
|  * @param {string[]} out
 | |
|  */
 | |
| function renderMember(member, parent, options, out) {
 | |
|   const name = toMemberName(member);
 | |
|   if (member.kind === 'method') {
 | |
|     renderMethod(member, parent, name, { mode: 'options', trimRunAndPrefix: options.trimRunAndPrefix }, out);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   let type = translateType(member.type, parent, t => generateNameDefault(member, name, t, parent));
 | |
|   if (member.kind === 'event') {
 | |
|     if (!member.type)
 | |
|       throw new Error(`No Event Type for ${name} in ${parent.name}`);
 | |
|     out.push('');
 | |
|     if (member.spec)
 | |
|       out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
 | |
|     if (member.deprecated)
 | |
|       out.push(`[System.Obsolete]`);
 | |
|     out.push(`event EventHandler<${type}> ${name};`);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (member.kind === 'property') {
 | |
|     if (parent && member && member.name === 'children') {  // this is a special hack for Accessibility
 | |
|       console.warn(`children property found in ${parent.name}, assuming array.`);
 | |
|       type = `IEnumerable<${parent.name}>`;
 | |
|     }
 | |
|     const overloads = getPropertyOverloads(type, member, name, parent);
 | |
|     for (const overload of overloads) {
 | |
|       const { name, jsonName } = overload;
 | |
|       let { type } = overload;
 | |
|       out.push('');
 | |
|       if (member.spec)
 | |
|         out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
 | |
|       if (!member.clazz)
 | |
|         out.push(`${member.required ? '[Required]\n' : ''}[JsonPropertyName("${jsonName}")]`);
 | |
|       if (member.deprecated)
 | |
|         out.push(`[System.Obsolete]`);
 | |
|       if (!type.endsWith('?') && !member.required)
 | |
|         type = `${type}?`;
 | |
|       const requiredSuffix = type.endsWith('?') ? '' : ' = default!;';
 | |
|       if (member.clazz)
 | |
|         out.push(`public ${type} ${name} { get; }`);
 | |
|       else
 | |
|         out.push(`public ${type} ${name} { get; set; }${requiredSuffix}`);
 | |
|     }
 | |
|     return;
 | |
|   }
 | |
|   throw new Error(`Problem rendering a member: ${type} - ${name} (${member.kind})`);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * @param {string} type
 | |
|  * @param {Documentation.Member} member
 | |
|  * @param {string} name
 | |
|  * @param {Documentation.Class|Documentation.Type} parent
 | |
|  * @returns [{ type: string; name: string; jsonName: string; }]
 | |
|  */
 | |
| function getPropertyOverloads(type, member, name, parent) {
 | |
|   const overloads = [];
 | |
|   if (type) {
 | |
|     let jsonName = member.name;
 | |
|     if (member.type.expression === '[string]|[float]')
 | |
|       jsonName = `${member.name}String`;
 | |
|     overloads.push({ type, name, jsonName });
 | |
|   } else {
 | |
|     for (const overload of member.type.union) {
 | |
|       const t = translateType(overload, parent, t => generateNameDefault(member, name, t, parent));
 | |
|       const suffix = toOverloadSuffix(t);
 | |
|       overloads.push({ type: t, name: name + suffix, jsonName: member.name + suffix });
 | |
|     }
 | |
|   }
 | |
|   return overloads;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * @param {Documentation.Member} member
 | |
|  * @param {string} name
 | |
|  * @param {Documentation.Type} t
 | |
|  * @param {*} parent
 | |
|  */
 | |
| function generateNameDefault(member, name, t, parent) {
 | |
|   if (!t.properties
 | |
|     && !t.templates
 | |
|     && !t.union
 | |
|     && t.expression === '[Object]')
 | |
|     return 'object';
 | |
| 
 | |
|   // we'd get this call for enums, primarily
 | |
|   const enumName = generateEnumNameIfApplicable(t);
 | |
|   if (!enumName && member) {
 | |
|     if (member.kind === 'method' || member.kind === 'property') {
 | |
|       const names = [
 | |
|         parent.alias || parent.name,
 | |
|         toTitleCase(member.alias || member.name),
 | |
|         toTitleCase(name),
 | |
|       ];
 | |
|       if (names[2] === names[1])
 | |
|         names.pop(); // get rid of duplicates, cheaply
 | |
|       let attemptedName = names.pop();
 | |
|       const typesDiffer = function(left, right) {
 | |
|         if (left.expression && right.expression)
 | |
|           return left.expression !== right.expression;
 | |
|         return JSON.stringify(right.properties) !== JSON.stringify(left.properties);
 | |
|       };
 | |
|       while (true) {
 | |
|         // crude attempt at removing plurality
 | |
|         if (attemptedName.endsWith('s')
 | |
|           && !['properties', 'httpcredentials'].includes(attemptedName.toLowerCase()))
 | |
|           attemptedName = attemptedName.substring(0, attemptedName.length - 1);
 | |
| 
 | |
|         // For some of these we don't want to generate generic types.
 | |
|         // For some others we simply did not have the code that was deduping the names.
 | |
|         if (attemptedName === 'BoundingBox')
 | |
|           attemptedName = `${parent.name}BoundingBoxResult`;
 | |
|         if (attemptedName === 'BrowserContextCookie')
 | |
|           attemptedName = 'BrowserContextCookiesResult';
 | |
|         if (attemptedName === 'File' || (parent.name === 'FormData' && attemptedName === 'SetValue'))
 | |
|           attemptedName = `FilePayload`;
 | |
|         if (attemptedName === 'Size')
 | |
|           attemptedName = 'RequestSizesResult';
 | |
|         if (attemptedName === 'ViewportSize' && parent.name === 'Page')
 | |
|           attemptedName = 'PageViewportSizeResult';
 | |
|         if (attemptedName === 'SecurityDetail')
 | |
|           attemptedName = 'ResponseSecurityDetailsResult';
 | |
|         if (attemptedName === 'ServerAddr')
 | |
|           attemptedName = 'ResponseServerAddrResult';
 | |
|         if (attemptedName === 'Timing')
 | |
|           attemptedName = 'RequestTimingResult';
 | |
|         if (attemptedName === 'HeadersArray')
 | |
|           attemptedName = 'Header';
 | |
| 
 | |
|         const probableType = modelTypes.get(attemptedName);
 | |
|         if ((probableType && typesDiffer(t, probableType))
 | |
|           || (['Value'].includes(attemptedName))) {
 | |
|           if (!names.length)
 | |
|             throw new Error(`Ran out of possible names: ${attemptedName}`);
 | |
|           attemptedName = `${names.pop()}${attemptedName}`;
 | |
|           continue;
 | |
|         } else {
 | |
|           registerModelType(attemptedName, t);
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|       return attemptedName;
 | |
|     }
 | |
| 
 | |
|     if (member.kind === 'event')
 | |
|       return `${name}Payload`;
 | |
| 
 | |
|   }
 | |
| 
 | |
|   return enumName || t.name;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * @param {Documentation.Type} type
 | |
|  * @returns
 | |
|  */
 | |
| function generateEnumNameIfApplicable(type) {
 | |
|   if (!type.union)
 | |
|     return null;
 | |
| 
 | |
|   const potentialValues = type.union.filter(u => u.name.startsWith('"'));
 | |
|   if ((potentialValues.length !== type.union.length)
 | |
|     && !(type.union[0].name === 'null' && potentialValues.length === type.union.length - 1))
 | |
|     return null; // this isn't an enum, so we don't care, we let the caller generate the name
 | |
| 
 | |
|   return type.name;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Rendering a method is so _special_, with so many weird edge cases, that it
 | |
|  * makes sense to put it separate from the other logic.
 | |
|  * @param {Documentation.Member} member
 | |
|  * @param {Documentation.Class | Documentation.Type} parent
 | |
|  * @param {string} name
 | |
|  * @param {{
 | |
|  *   mode: 'options'|'named'|'base',
 | |
|  *   nodocs?: boolean,
 | |
|  *   abstract?: boolean,
 | |
|  *   public?: boolean,
 | |
|  *   trimRunAndPrefix?: boolean,
 | |
|  * }} options
 | |
|  * @param {string[]} out
 | |
|  */
 | |
| function renderMethod(member, parent, name, options, out) {
 | |
|   out.push('');
 | |
| 
 | |
|   if (options.trimRunAndPrefix)
 | |
|     name = name.substring('RunAnd'.length);
 | |
| 
 | |
|   /** @type {Map<string, string[]>} */
 | |
|   const paramDocs = new Map();
 | |
|   const addParamsDoc = (paramName, docs) => {
 | |
|     if (paramName.startsWith('@'))
 | |
|       paramName = paramName.substring(1);
 | |
|     if (paramDocs.get(paramName) && paramDocs.get(paramName) !== docs)
 | |
|       throw new Error(`Parameter ${paramName} already exists in the docs.`);
 | |
|     paramDocs.set(paramName, docs);
 | |
|   };
 | |
| 
 | |
|   let type = translateType(member.type, parent, t => generateNameDefault(member, name, t, parent), false, true);
 | |
| 
 | |
|   // TODO: this is something that will probably go into the docs
 | |
|   // translate simple getters into read-only properties, and simple
 | |
|   // set-only methods to settable properties
 | |
|   if (member.args.size === 0
 | |
|     && type !== 'void'
 | |
|     && !name.startsWith('Get')
 | |
|     && name !== 'CreateFormData'
 | |
|     && !name.startsWith('PostDataJSON')
 | |
|     && !name.startsWith('As')) {
 | |
|     if (!member.async) {
 | |
|       if (member.spec && !options.nodocs)
 | |
|         out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
 | |
|       if (member.deprecated)
 | |
|         out.push(`[System.Obsolete]`);
 | |
|       out.push(`${type} ${name} { get; }`);
 | |
|       return;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // HACK: special case for generics handling!
 | |
|   if (type === 'T')
 | |
|     name = `${name}<T>`;
 | |
| 
 | |
| 
 | |
|   // adjust the return type for async methods
 | |
|   if (member.async) {
 | |
|     if (type === 'void')
 | |
|       type = `Task`;
 | |
|     else
 | |
|       type = `Task<${type}>`;
 | |
|   }
 | |
| 
 | |
|   // render args
 | |
|   /** @type {string[]} */
 | |
|   const args = [];
 | |
|   /** @type {string[]} */
 | |
|   const explodedArgs = [];
 | |
|   /** @type {Map<string, string>} */
 | |
|   const argTypeMap = new Map([]);
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} innerArgType
 | |
|    * @param {string} innerArgName
 | |
|    * @param {Documentation.Member} argument
 | |
|    * @param {boolean} isExploded
 | |
|    */
 | |
|   function pushArg(innerArgType, innerArgName, argument, isExploded = false) {
 | |
|     if (innerArgType === 'null')
 | |
|       return;
 | |
|     const requiredPrefix = (argument.required || isExploded) ? '' : '?';
 | |
|     const requiredSuffix = (argument.required || isExploded) ? '' : ' = default';
 | |
|     const push = `${innerArgType}${requiredPrefix} ${innerArgName}${requiredSuffix}`;
 | |
|     if (isExploded)
 | |
|       explodedArgs.push(push);
 | |
|     else
 | |
|       args.push(push);
 | |
|     argTypeMap.set(push, innerArgName);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {Documentation.Member} arg
 | |
|    */
 | |
|   function processArg(arg) {
 | |
|     if (options.trimRunAndPrefix && arg.name === 'action')
 | |
|       return;
 | |
| 
 | |
|     if (arg.name === 'options') {
 | |
|       if (options.mode === 'options' || options.mode === 'base') {
 | |
|         const optionsType = rewriteSuggestedOptionsName(member.clazz.name + name.replace('<T>', '') + 'Options');
 | |
|         if (!optionTypes.has(optionsType) || arg.type.properties.length > optionTypes.get(optionsType).properties.length)
 | |
|           optionTypes.set(optionsType, arg.type);
 | |
|         args.push(`${optionsType}? options = default`);
 | |
|         argTypeMap.set(`${optionsType}? options = default`, 'options');
 | |
|         addParamsDoc('options', ['Call options']);
 | |
|       } else {
 | |
|         arg.type.properties.forEach(processArg);
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (arg.type.expression === '[string]|[path]') {
 | |
|       const argName = toArgumentName(arg.name);
 | |
|       pushArg('string?', `${argName} = default`, arg);
 | |
|       pushArg('string?', `${argName}Path = default`, arg);
 | |
|       if (arg.spec) {
 | |
|         addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
 | |
|         addParamsDoc(`${argName}Path`, [`Instead of specifying <paramref name="${argName}"/>, gives the file name to load from.`]);
 | |
|       }
 | |
|       return;
 | |
|     } else if (arg.type.expression === '[boolean]|[Array]<[string]>') {
 | |
|       // HACK: this hurts my brain too
 | |
|       // we split this into two args, one boolean, with the logical name
 | |
|       const argName = toArgumentName(arg.name);
 | |
|       const leftArgType = translateType(arg.type.union[0], parent, t => { throw new Error('Not supported'); });
 | |
|       const rightArgType = translateType(arg.type.union[1], parent, t => { throw new Error('Not supported'); });
 | |
| 
 | |
|       pushArg(leftArgType, argName, arg);
 | |
|       pushArg(rightArgType, `${argName}Values`, arg);
 | |
| 
 | |
|       addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
 | |
|       addParamsDoc(`${argName}Values`, [`The values to take into account when <paramref name="${argName}"/> is <code>true</code>.`]);
 | |
| 
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const argName = toArgumentName(arg.alias || arg.name);
 | |
|     const argType = translateType(arg.type, parent, t => generateNameDefault(member, argName, t, parent));
 | |
| 
 | |
|     if (argType === null && arg.type.union) {
 | |
|       // we might have to split this into multiple arguments
 | |
|       const translatedArguments = arg.type.union.map(t => translateType(t, parent, x => generateNameDefault(member, argName, x, parent)));
 | |
|       if (translatedArguments.includes(null))
 | |
|         throw new Error('Unexpected null in translated argument types. Aborting.');
 | |
| 
 | |
|       const argDocumentation = XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth);
 | |
|       for (const newArg of translatedArguments) {
 | |
|         pushArg(newArg, argName, arg, true); // push the exploded arg
 | |
|         addParamsDoc(argName, argDocumentation);
 | |
|       }
 | |
|       args.push(arg.required ? 'EXPLODED_ARG' : 'OPTIONAL_EXPLODED_ARG');
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     addParamsDoc(argName, XmlDoc.renderTextOnly(arg.spec, maxDocumentationColumnWidth));
 | |
| 
 | |
|     if (argName === 'timeout' && argType === 'decimal') {
 | |
|       args.push(`int timeout = 0`); // a special argument, we ignore our convention
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     pushArg(argType, argName, arg);
 | |
|   }
 | |
| 
 | |
|   let modifiers = '';
 | |
|   if (options.abstract)
 | |
|     modifiers = 'protected abstract ';
 | |
|   if (options.public)
 | |
|     modifiers = 'public ';
 | |
| 
 | |
|   member.argsArray
 | |
|       .sort((a, b) => b.alias === 'options' ? -1 : 0) // move options to the back to the arguments list
 | |
|       .forEach(processArg);
 | |
| 
 | |
|   let body = ';';
 | |
|   if (options.mode === 'base') {
 | |
|     // Generate options -> named transition.
 | |
|     const tokens = [];
 | |
|     for (const arg of member.argsArray) {
 | |
|       if (arg.name === 'action' && options.trimRunAndPrefix)
 | |
|         continue;
 | |
|       if (arg.name !== 'options') {
 | |
|         tokens.push(toArgumentName(arg.name));
 | |
|         continue;
 | |
|       }
 | |
|       for (const opt of arg.type.properties) {
 | |
|         // TODO: use translate type here?
 | |
|         if (opt.type.union && !opt.type.union[0].name.startsWith('"') && opt.type.union[0].name !== 'null' && opt.type.expression !== '[string]|[Buffer]') {
 | |
|           // Explode overloads.
 | |
|           for (const t of opt.type.union) {
 | |
|             const suffix = toOverloadSuffix(translateType(t, parent));
 | |
|             tokens.push(`${opt.name}${suffix}: options.${toMemberName(opt)}${suffix}`);
 | |
|           }
 | |
|         } else {
 | |
|           tokens.push(`${opt.alias || opt.name}: options.${toMemberName(opt)}`);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     body = `
 | |
| {
 | |
|     options ??= new ${member.clazz.name}${name}Options();
 | |
|     return ${toAsync(name, member.async)}(${tokens.join(', ')});
 | |
| }`;
 | |
|   }
 | |
| 
 | |
|   if (!explodedArgs.length) {
 | |
|     if (!options.nodocs) {
 | |
|       out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
 | |
|       paramDocs.forEach((value, i) => printArgDoc(i, value, out));
 | |
|     }
 | |
|     if (member.deprecated)
 | |
|       out.push(`[System.Obsolete]`);
 | |
|     out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${args.join(', ')})${body}`);
 | |
|   } else {
 | |
|     let containsOptionalExplodedArgs = false;
 | |
|     explodedArgs.forEach((explodedArg, argIndex) => {
 | |
|       if (!options.nodocs)
 | |
|         out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
 | |
|       const overloadedArgs = [];
 | |
|       for (let i = 0; i < args.length; i++) {
 | |
|         const arg = args[i];
 | |
|         if (arg === 'EXPLODED_ARG' || arg === 'OPTIONAL_EXPLODED_ARG') {
 | |
|           containsOptionalExplodedArgs = arg === 'OPTIONAL_EXPLODED_ARG';
 | |
|           const argType = argTypeMap.get(explodedArg);
 | |
|           if (!options.nodocs)
 | |
|             printArgDoc(argType, paramDocs.get(argType), out);
 | |
|           overloadedArgs.push(explodedArg);
 | |
|         } else {
 | |
|           const argType = argTypeMap.get(arg);
 | |
|           if (!options.nodocs)
 | |
|             printArgDoc(argType, paramDocs.get(argType), out);
 | |
|           overloadedArgs.push(arg);
 | |
|         }
 | |
|       }
 | |
|       out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${overloadedArgs.join(', ')})${body}`);
 | |
|       if (argIndex < explodedArgs.length - 1)
 | |
|         out.push(''); // output a special blank line
 | |
|     });
 | |
| 
 | |
|     // If the exploded union arguments are optional, we also output a special
 | |
|     // signature, to help prevent compilation errors with ambiguous overloads.
 | |
|     // That particular overload only contains the required arguments, or rather
 | |
|     // contains all the arguments *except* the exploded ones.
 | |
|     if (containsOptionalExplodedArgs) {
 | |
|       const filteredArgs = args.filter(x => x !== 'OPTIONAL_EXPLODED_ARG');
 | |
|       if (!options.nodocs)
 | |
|         out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
 | |
|       filteredArgs.forEach(arg => {
 | |
|         if (arg === 'EXPLODED_ARG')
 | |
|           throw new Error(`Unsupported required union arg combined an optional union inside ${member.name}`);
 | |
|         const argType = argTypeMap.get(arg);
 | |
|         if (!options.nodocs)
 | |
|           printArgDoc(argType, paramDocs.get(argType), out);
 | |
|       });
 | |
|       out.push(`${type} ${name}(${filteredArgs.join(', ')})${body}`);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  *  @param {Documentation.Type} type
 | |
|  *  @param {Documentation.Class|Documentation.Type} parent
 | |
|  *  @param {function(Documentation.Type): string} generateNameCallback
 | |
|  *  @param {boolean=} optional
 | |
|  *  @returns {string}
 | |
|  */
 | |
| function translateType(type, parent, generateNameCallback = t => t.name, optional = false, isReturnType = false) {
 | |
|   // a few special cases we can fix automatically
 | |
|   if (type.expression === '[null]|[Error]')
 | |
|     return 'void';
 | |
| 
 | |
|   if (type.union) {
 | |
|     if (type.union[0].name === 'null' && type.union.length === 2)
 | |
|       return translateType(type.union[1], parent, generateNameCallback, true, isReturnType);
 | |
| 
 | |
|     if (type.expression === '[string]|[Buffer]')
 | |
|       return `byte[]`; // TODO: make sure we implement extension methods for this!
 | |
|     if (type.expression === '[string]|[float]' || type.expression === '[string]|[float]|[boolean]') {
 | |
|       console.warn(`${type.name} should be a 'string', but was a ${type.expression}`);
 | |
|       return `string`;
 | |
|     }
 | |
|     if (type.union.length === 2 && type.union[1].name === 'Array' && type.union[1].templates[0].name === type.union[0].name)
 | |
|       return `IEnumerable<${type.union[0].name}>`; // an example of this is [string]|[Array]<[string]>
 | |
|     if (type.expression === '[float]|"raf"')
 | |
|       return `Polling`; // hardcoded because there's no other way to denote this
 | |
| 
 | |
|     // Regular primitive enums are named in the markdown.
 | |
|     if (type.name) {
 | |
|       enumTypes.set(type.name, type.union.map(t => t.name));
 | |
|       return optional ? type.name + '?' : type.name;
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   if (type.name === 'Array') {
 | |
|     if (type.templates.length !== 1)
 | |
|       throw new Error(`Array (${type.name} from ${parent.name}) has more than 1 dimension. Panic.`);
 | |
| 
 | |
|     const innerType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
 | |
|     return isReturnType ? `IReadOnlyList<${innerType}>` : `IEnumerable<${innerType}>`;
 | |
|   }
 | |
| 
 | |
|   if (type.name === 'Object') {
 | |
|     // take care of some common cases
 | |
|     // TODO: this can be genericized
 | |
|     if (type.templates && type.templates.length === 2) {
 | |
|       // get the inner types of both templates, and if they're strings, it's a keyvaluepair string, string,
 | |
|       const keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
 | |
|       const valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
 | |
|       if (['Request', 'Response', 'APIResponse'].includes(parent.name))
 | |
|         return `Dictionary<${keyType}, ${valueType}>`;
 | |
|       return `IEnumerable<KeyValuePair<${keyType}, ${valueType}>>`;
 | |
|     }
 | |
| 
 | |
|     if ((type.name === 'Object')
 | |
|       && !type.properties
 | |
|       && !type.union)
 | |
|       return 'object';
 | |
| 
 | |
|     // this is an additional type that we need to generate
 | |
|     const objectName = generateNameCallback(type);
 | |
|     if (objectName === 'Object')
 | |
|       throw new Error('Object unexpected');
 | |
|     else if (type.name === 'Object')
 | |
|       registerModelType(objectName, type);
 | |
| 
 | |
|     return `${objectName}${optional ? '?' : ''}`;
 | |
|   }
 | |
| 
 | |
|   if (type.name === 'Map') {
 | |
|     if (type.templates && type.templates.length === 2) {
 | |
|       // we map to a dictionary
 | |
|       const keyType = translateType(type.templates[0], parent, generateNameCallback, false, isReturnType);
 | |
|       const valueType = translateType(type.templates[1], parent, generateNameCallback, false, isReturnType);
 | |
|       return `Dictionary<${keyType}, ${valueType}>`;
 | |
|     } else {
 | |
|       throw 'Map has invalid number of templates.';
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (type.name === 'function') {
 | |
|     if (type.expression === '[function]' || !type.args)
 | |
|       return 'Action'; // super simple mapping
 | |
| 
 | |
|     let argsList = '';
 | |
|     if (type.args) {
 | |
|       const translatedCallbackArguments = type.args.map(t => translateType(t, parent, generateNameCallback, false, isReturnType));
 | |
|       if (translatedCallbackArguments.includes(null))
 | |
|         throw new Error('There was an argument we could not parse. Aborting.');
 | |
| 
 | |
|       argsList = translatedCallbackArguments.join(', ');
 | |
|     }
 | |
| 
 | |
|     if (!type.returnType) {
 | |
|       // this is an Action
 | |
|       return `Action<${argsList}>`;
 | |
|     } else {
 | |
|       const returnType = translateType(type.returnType, parent, generateNameCallback, false, isReturnType);
 | |
|       if (returnType === null)
 | |
|         throw new Error('Unexpected null as return type.');
 | |
| 
 | |
|       return `Func<${argsList}, ${returnType}>`;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (type.templates) {
 | |
|     // this should mean we have a generic type and we can translate that
 | |
|     /** @type {string[]} */
 | |
|     const types = type.templates.map(template => translateType(template, parent));
 | |
|     return `${type.name}<${types.join(', ')}>`;
 | |
|   }
 | |
| 
 | |
|   if (type.name === 'Serializable')
 | |
|     return isReturnType ? 'T' : 'object';
 | |
| 
 | |
|   // there's a chance this is a name we've already seen before, so check
 | |
|   // this is also where we map known types, like boolean -> bool, etc.
 | |
|   const name = classNameMap.get(type.name) || type.name;
 | |
|   return `${name}${optional ? '?' : ''}`;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} typeName
 | |
|  * @param {Documentation.Type} type
 | |
|  */
 | |
| function registerModelType(typeName, type) {
 | |
|   if (['object', 'string', 'int'].includes(typeName))
 | |
|     return;
 | |
|   if (typeName.endsWith('Option'))
 | |
|     return;
 | |
| 
 | |
|   const potentialType = modelTypes.get(typeName);
 | |
|   if (potentialType) {
 | |
|     // console.log(`Type ${typeName} already exists, so skipping...`);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   modelTypes.set(typeName, type);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} name
 | |
|  * @param {string[]} value
 | |
|  * @param {string[]} out
 | |
|  */
 | |
| function printArgDoc(name, value, out) {
 | |
|   if (value.length === 1) {
 | |
|     out.push(`/// <param name="${name}">${value}</param>`);
 | |
|   } else {
 | |
|     out.push(`/// <param name="${name}">`);
 | |
|     out.push(...value.map(l => `/// ${l}`));
 | |
|     out.push(`/// </param>`);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} typeName
 | |
|  * @return {string}
 | |
|  */
 | |
| function toOverloadSuffix(typeName) {
 | |
|   return toTitleCase(typeName.replace(/[<].*[>]/, '').replace(/[^a-zA-Z]/g, ''));
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} name
 | |
|  * @param {boolean} convert
 | |
|  */
 | |
| function toAsync(name, convert) {
 | |
|   if (!convert)
 | |
|     return name;
 | |
|   if (name.includes('<'))
 | |
|     return name.replace('<', 'Async<');
 | |
|   return name + 'Async';
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {string} suggestedName 
 | |
|  * @returns {string}
 | |
|  */
 | |
| function rewriteSuggestedOptionsName(suggestedName) {
 | |
|   if ([
 | |
|     'APIRequestContextDeleteOptions',
 | |
|     'APIRequestContextFetchOptions',
 | |
|     'APIRequestContextGetOptions',
 | |
|     'APIRequestContextHeadOptions',
 | |
|     'APIRequestContextPatchOptions',
 | |
|     'APIRequestContextPostOptions',
 | |
|     'APIRequestContextPutOptions',
 | |
|   ].includes(suggestedName))
 | |
|     return 'APIRequestContextOptions';
 | |
|   return suggestedName;
 | |
| }
 | |
| 
 | 
