Parse module associated named types and generate in modules (#16)

* Parse module associated named types

* Generate associated types in module

* Lint

* Serialize associated types

* Lint

* Support multiple module paths

* Fix demo.ts

* Modify template

* Improve print

* Lower cased enum

* Uncapitalize enum

* Update template

* Remove gulp

* Cleanup
This commit is contained in:
Zhuoran 2021-06-03 17:33:46 +08:00 коммит произвёл GitHub
Родитель a7c6cc4a9c
Коммит 8437bf3d95
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 246 добавлений и 6957 удалений

Просмотреть файл

@ -2,6 +2,7 @@
// Copyright 2013-2018 Microsoft Inc. // Copyright 2013-2018 Microsoft Inc.
// //
// swiftformat:disable redundantRawValues
// Don't modify this file manually, it's auto generated. // Don't modify this file manually, it's auto generated.
public class {{moduleName}} { public class {{moduleName}} {
@ -34,3 +35,7 @@ public class {{moduleName}} {
} }
{{/methods}} {{/methods}}
} }
{{#associatedTypes}}
{{> swift-named-type}}
{{/associatedTypes}}

Просмотреть файл

@ -1,13 +1,4 @@
// {{#custom}}
// Copyright 2013-2018 Microsoft Inc.
//
// swiftformat:disable redundantRawValues
// Don't modify this file manually, it's auto generated.
import UIKit
{{#customTypes}}
public struct {{typeName}}: Codable { public struct {{typeName}}: Codable {
{{#members}} {{#members}}
public var {{name}}: {{type}} public var {{name}}: {{type}}
@ -19,12 +10,11 @@ public struct {{typeName}}: Codable {
{{/members}} {{/members}}
} }
} }
{{/customTypes}} {{/custom}}
{{#enumTypes}} {{#enum}}
public enum {{typeName}}: {{valueType}}, Codable { public enum {{typeName}}: {{valueType}}, Codable {
{{#members}} {{#members}}
case {{key}} = {{{value}}} case {{key}} = {{{value}}}
{{/members}} {{/members}}
} }
{{/enumTypes}} {{/enum}}

Просмотреть файл

@ -0,0 +1,12 @@
//
// Copyright 2013-2018 Microsoft Inc.
//
// swiftformat:disable redundantRawValues
// Don't modify this file manually, it's auto generated.
import UIKit
{{#.}}
{{> swift-named-type}}
{{/.}}

Просмотреть файл

@ -2,6 +2,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved. // Copyright (c) Microsoft Corporation. All rights reserved.
// //
// swiftformat:disable redundantRawValues
import Foundation import Foundation
public protocol {{moduleName}}: EditorNativeModule { public protocol {{moduleName}}: EditorNativeModule {
@ -46,6 +48,7 @@ class {{customTags.bridgeName}}: NativeModuleBridge {
parameters = try decoder.decode(Parameters.self, from: parametersData) parameters = try decoder.decode(Parameters.self, from: parametersData)
} }
catch { catch {
logAssertFail("Parameters of {{methodName}} are invalid: \(error)")
completion(.failure(NativeMethodError.invalidParameters(parametersData))) completion(.failure(NativeMethodError.invalidParameters(parametersData)))
return return
} }
@ -57,3 +60,7 @@ class {{customTags.bridgeName}}: NativeModuleBridge {
} }
{{/methods}} {{/methods}}
} }
{{#associatedTypes}}
{{> swift-named-type}}
{{/associatedTypes}}

Просмотреть файл

@ -1,36 +0,0 @@
import {series, watch, dest} from 'gulp'
import ts from 'gulp-typescript'
import del from 'del'
import {spawn} from 'child_process'
const tsProject = ts.createProject("tsconfig.json", {declaration: true})
function tsBuild() {
return tsProject
.src()
.pipe(tsProject())
.pipe(dest("dist"))
}
async function run() {
spawn('node', ['dist/index.js'], {stdio: 'inherit'})
}
async function tsRun() {
spawn('ts-node', ['src/index.ts'], {stdio: 'inherit'})
}
function watchRun() {
watch('src/*.ts', tsRun)
}
export const build = tsBuild
export function clean() {
return del('dist')
}
export const start = series(clean, build, run)
export const dev = series(tsRun, watchRun)

3046
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -3,14 +3,12 @@
"version": "0.1.11", "version": "0.1.11",
"description": "Generate Native API based on TS interface", "description": "Generate Native API based on TS interface",
"scripts": { "scripts": {
"build": "gulp build", "build": "tsc",
"clean": "gulp clean", "clean": "rm -rf dist",
"start": "gulp start",
"dev": "gulp dev",
"debug": "ts-node ./src/index", "debug": "ts-node ./src/index",
"start:example": "ts-node ./src/demo/demo", "start:example": "ts-node ./src/demo/demo",
"test": "mocha -r ts-node/register test/**/*.ts", "test": "mocha -r ts-node/register test/**/*.ts",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", "lint": "eslint ./src --ext .js,.ts",
"prettier:write": "prettier --write \"src/**/*.ts\"", "prettier:write": "prettier --write \"src/**/*.ts\"",
"prettier:check": "prettier --check \"src/**/*.ts\"", "prettier:check": "prettier --check \"src/**/*.ts\"",
"lint:fix": "npm run lint -- --fix && npm run prettier:write", "lint:fix": "npm run lint -- --fix && npm run prettier:write",
@ -30,7 +28,6 @@
"license": "private", "license": "private",
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/gulp": "^4.0.6",
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
"@types/mustache": "^4.1.1", "@types/mustache": "^4.1.1",
"@types/sinon": "^10.0.1", "@types/sinon": "^10.0.1",
@ -45,8 +42,6 @@
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^7.0.0", "eslint-config-prettier": "^7.0.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"gulp": "^4.0.2",
"gulp-typescript": "^6.0.0-alpha.1",
"mocha": "^8.4.0", "mocha": "^8.4.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"sinon": "^11.1.1", "sinon": "^11.1.1",

Просмотреть файл

@ -2,13 +2,16 @@ import { CodeGenerator, RenderingLanguage } from '../generator/CodeGenerator';
function run(): void { function run(): void {
const generator = new CodeGenerator(); const generator = new CodeGenerator();
generator.parse({ tag: 'APIs', interfacePaths: ['src/demo/data/demoApi.ts'], defaultCustomTags: {}, dropInterfaceIPrefix: true }); generator.parse({
generator.render({ interfacePaths: ['src/demo/data/demoApi.ts'],
tag: 'APIs', defaultCustomTags: {},
dropInterfaceIPrefix: true,
});
generator.renderModules({
index: 0,
language: RenderingLanguage.Swift, language: RenderingLanguage.Swift,
outputDirectory: 'generated', outputDirectory: 'generated',
moduleTemplatePath: 'templates/swift-bridge.mustache', moduleTemplatePath: 'templates/swift-bridge.mustache',
namedTypesTemplatePath: 'templates/swift-named-types.mustache',
}); });
} }

Просмотреть файл

@ -1,12 +1,12 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { dropIPrefixInCustomTypes, fetchNamedTypes, NamedType } from './named-types'; import { dropIPrefixInCustomTypes, fetchNamedTypes, NamedType, NamedTypesResult } from './named-types';
import { Parser } from '../parser/Parser'; import { Parser } from '../parser/Parser';
import { renderCode } from '../renderer/renderer'; import { renderCode } from '../renderer/renderer';
import { SwiftCustomTypeView } from '../renderer/swift/SwiftCustomTypeView'; import { SwiftCustomTypeView } from '../renderer/swift/SwiftCustomTypeView';
import { SwiftEnumTypeView } from '../renderer/swift/SwiftEnumTypeView'; import { SwiftEnumTypeView } from '../renderer/swift/SwiftEnumTypeView';
import { SwiftModuleView } from '../renderer/swift/SwiftModuleView'; import { SwiftModuleView } from '../renderer/swift/SwiftModuleView';
import { CustomTypeView, EnumTypeView, ModuleView, NamedTypesView } from '../renderer/views'; import { CustomTypeView, EnumTypeView, ModuleView, NamedTypeView } from '../renderer/views';
import { serializeModule, serializeNamedType } from '../serializers'; import { serializeModule, serializeNamedType } from '../serializers';
import { CustomType, EnumType, isCustomType, Module } from '../types'; import { CustomType, EnumType, isCustomType, Module } from '../types';
import { applyDefaultCustomTags } from './utils'; import { applyDefaultCustomTags } from './utils';
@ -16,20 +16,18 @@ export enum RenderingLanguage {
} }
export class CodeGenerator { export class CodeGenerator {
private modulesMap: Record<string, Module[]> = {}; private modulesMap: Module[][] = [];
private namedTypes: Record<string, NamedType> = {}; private namedTypes?: NamedTypesResult;
parse({ parse({
tag,
interfacePaths, interfacePaths,
defaultCustomTags, defaultCustomTags,
dropInterfaceIPrefix, dropInterfaceIPrefix,
}: { }: {
tag: string;
interfacePaths: string[]; interfacePaths: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultCustomTags: Record<string, any>, defaultCustomTags: Record<string, any>;
dropInterfaceIPrefix: boolean; dropInterfaceIPrefix: boolean;
}): void { }): void {
const parser = new Parser(interfacePaths); const parser = new Parser(interfacePaths);
@ -41,59 +39,80 @@ export class CodeGenerator {
dropIPrefixInCustomTypes(modules); dropIPrefixInCustomTypes(modules);
} }
const namedTypes = fetchNamedTypes(modules); this.modulesMap.push(modules);
this.modulesMap[tag] = modules;
this.pushNamedTypes(namedTypes);
} }
printModules({ tag }: { tag: string }): void { parseNamedTypes(): void {
const modules = this.modulesMap[tag]; this.namedTypes = fetchNamedTypes(Object.values(this.modulesMap).flatMap((modules) => modules));
}
printModules(index: number): void {
const modules = this.modulesMap[index];
if (modules === undefined) { if (modules === undefined) {
throw Error('Modules not parsed. Run parse first.'); throw Error('Modules not parsed. Run parse first.');
} }
console.log('Modules:\n'); console.log('Modules:\n');
console.log(modules.map((module) => serializeModule(module)).join('\n\n'));
}
printNamedTypes(): void {
console.log('\nNamed types:\n');
console.log( console.log(
Object.entries(this.namedTypes) modules.map((module) => serializeModule(module, this.namedTypes?.associatedTypes[module.name] ?? [])).join('\n\n')
.map(([typeName, namedType]) => serializeNamedType(typeName, namedType))
.join('\n\n')
); );
console.log();
} }
render({ printSharedNamedTypes(): void {
tag, if (this.namedTypes === undefined) {
throw Error('Named types not parsed. Run parseNamedTypes first.');
}
console.log('Shared named types:\n');
console.log(this.namedTypes.sharedTypes.map((namedType) => serializeNamedType(namedType)).join('\n\n'));
}
renderModules({
index,
language, language,
outputDirectory, outputDirectory,
moduleTemplatePath, moduleTemplatePath,
namedTypesTemplatePath,
}: { }: {
tag: string; index: number;
language: RenderingLanguage; language: RenderingLanguage;
outputDirectory: string; outputDirectory: string;
moduleTemplatePath: string; moduleTemplatePath: string;
namedTypesTemplatePath: string;
}): void { }): void {
const modules = this.modulesMap[tag]; const modules = this.modulesMap[index];
if (modules === undefined) { if (modules === undefined) {
throw Error('Modules not parsed. Run parse first.'); throw Error('Modules not parsed. Run parse first.');
} }
if (this.namedTypes === undefined) {
throw Error('Named types not parsed. Run parseNamedTypes first.');
}
const { associatedTypes } = this.namedTypes;
modules.forEach((module) => { modules.forEach((module) => {
const moduleView = this.getModuleView(language, module); const moduleView = this.getModuleView(language, module, associatedTypes[module.name] ?? []);
const renderedCode = renderCode(moduleTemplatePath, moduleView); const renderedCode = renderCode(moduleTemplatePath, moduleView);
this.writeFile(renderedCode, outputDirectory, `${moduleView.moduleName}${this.getFileExtension(language)}`); this.writeFile(renderedCode, outputDirectory, `${moduleView.moduleName}${this.getFileExtension(language)}`);
}); });
}
const namedTypesView = this.getNamedTypesView(language, this.namedTypes); renderNamedTypes({
language,
namedTypesTemplatePath,
namedTypesOutputPath,
}: {
language: RenderingLanguage;
namedTypesTemplatePath: string;
namedTypesOutputPath: string;
}): void {
if (this.namedTypes === undefined) {
throw Error('Named types not parsed. Run parseNamedTypes first.');
}
const namedTypesView = this.namedTypes.sharedTypes.map((namedType) => this.getNamedTypeView(language, namedType));
const renderedCode = renderCode(namedTypesTemplatePath, namedTypesView); const renderedCode = renderCode(namedTypesTemplatePath, namedTypesView);
this.writeFile(renderedCode, outputDirectory, `Generated_CustomInterface${this.getFileExtension(language)}`); fs.writeFileSync(namedTypesOutputPath, renderedCode);
} }
private getFileExtension(language: RenderingLanguage): string { private getFileExtension(language: RenderingLanguage): string {
@ -105,24 +124,26 @@ export class CodeGenerator {
} }
} }
private getNamedTypesView(language: RenderingLanguage, namedTypes: Record<string, NamedType>): NamedTypesView { private getNamedTypeView(language: RenderingLanguage, namedType: NamedType): NamedTypeView {
const namedTypesView: NamedTypesView = { customTypes: [], enumTypes: [] }; let namedTypeView: NamedTypeView;
if (isCustomType(namedType)) {
namedTypeView = this.getCustomTypeView(language, namedType.name, namedType);
namedTypeView.custom = true;
} else {
namedTypeView = this.getEnumTypeView(language, namedType);
namedTypeView.enum = true;
}
Object.entries(namedTypes).forEach(([typeName, namedType]) => { return namedTypeView;
if (isCustomType(namedType)) {
namedTypesView.customTypes.push(this.getCustomTypeView(language, typeName, namedType));
} else {
namedTypesView.enumTypes.push(this.getEnumTypeView(language, typeName, namedType));
}
});
return namedTypesView;
} }
private getModuleView(language: RenderingLanguage, module: Module): ModuleView { private getModuleView(language: RenderingLanguage, module: Module, associatedTypes: NamedType[]): ModuleView {
switch (language) { switch (language) {
case RenderingLanguage.Swift: case RenderingLanguage.Swift:
return new SwiftModuleView(module); return new SwiftModuleView(
module,
associatedTypes.map((associatedType) => this.getNamedTypeView(language, associatedType))
);
default: default:
throw Error('Unhandled language'); throw Error('Unhandled language');
} }
@ -137,10 +158,10 @@ export class CodeGenerator {
} }
} }
private getEnumTypeView(language: RenderingLanguage, typeName: string, enumType: EnumType): EnumTypeView { private getEnumTypeView(language: RenderingLanguage, enumType: EnumType): EnumTypeView {
switch (language) { switch (language) {
case RenderingLanguage.Swift: case RenderingLanguage.Swift:
return new SwiftEnumTypeView(typeName, enumType); return new SwiftEnumTypeView(enumType);
default: default:
throw Error('Unhandled language'); throw Error('Unhandled language');
} }
@ -150,14 +171,4 @@ export class CodeGenerator {
const filePath = path.join(outputDirectory, fileName); const filePath = path.join(outputDirectory, fileName);
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
} }
private pushNamedTypes(namedTypes: Record<string, NamedType>): void {
Object.entries(namedTypes).forEach(([typeName, namedType]) => {
if (this.namedTypes[typeName] !== undefined) {
return;
}
this.namedTypes[typeName] = namedType;
});
}
} }

Просмотреть файл

@ -1,7 +1,5 @@
import { capitalize } from '../utils'; import { capitalize } from '../utils';
import { import {
CustomType,
EnumType,
isArraryType, isArraryType,
isCustomType, isCustomType,
isDictionaryType, isDictionaryType,
@ -9,53 +7,74 @@ import {
isOptionalType, isOptionalType,
Module, Module,
ValueType, ValueType,
CustomType,
EnumType,
} from '../types'; } from '../types';
export type NamedType = CustomType | EnumType; export type NamedType = (CustomType & { name: string }) | EnumType;
export type NamedTypesResult = { associatedTypes: Record<string, NamedType[]>; sharedTypes: NamedType[] };
export function dropIPrefixInCustomTypes(modules: Module[]): void { export function dropIPrefixInCustomTypes(modules: Module[]): void {
fetchRootTypes(modules).forEach((valueType) => { modules
recursiveVisitNamedType(valueType, (namedType) => { .flatMap((module) => fetchRootTypes(module))
if (!isCustomType(namedType)) { .forEach((valueType) => {
return; recursiveVisitNamedType(valueType, (namedType) => {
} if (!isCustomType(namedType)) {
return;
}
namedType.name = namedType.name?.replace(/^I/, ''); namedType.name = namedType.name?.replace(/^I/, '');
});
}); });
});
} }
export function fetchNamedTypes(modules: Module[]): Record<string, NamedType> { export function fetchNamedTypes(modules: Module[]): NamedTypesResult {
const typeMap: Record<string, NamedType> = {}; const typeMap: Record<string, { namedType: NamedType; associatedModules: Set<string> }> = {};
fetchRootTypes(modules).forEach((valueType) => { modules.forEach((module) => {
recursiveVisitNamedType(valueType, (namedType, path) => { fetchRootTypes(module).forEach((valueType) => {
if (namedType.name === undefined) { recursiveVisitNamedType(valueType, (namedType, path) => {
namedType.name = path; if (namedType.name === undefined) {
} namedType.name = path;
}
if (typeMap[namedType.name] !== undefined) { if (typeMap[namedType.name] === undefined) {
return; typeMap[namedType.name] = { namedType: namedType as NamedType, associatedModules: new Set() };
} }
typeMap[namedType.name] = namedType; typeMap[namedType.name].associatedModules.add(module.name);
});
}); });
}); });
return typeMap; const associatedTypes: Record<string, NamedType[]> = {};
const sharedTypes: NamedType[] = [];
Object.values(typeMap).forEach(({ namedType, associatedModules }) => {
if (associatedModules.size === 1) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const moduleName: string = associatedModules.values().next().value;
if (associatedTypes[moduleName] === undefined) {
associatedTypes[moduleName] = [];
}
associatedTypes[moduleName].push(namedType);
} else {
sharedTypes.push(namedType);
}
});
return { associatedTypes, sharedTypes };
} }
function fetchRootTypes(modules: Module[]): ValueType[] { function fetchRootTypes(module: Module): ValueType[] {
return modules return module.methods.flatMap((method) =>
.flatMap((module) => module.methods) method.parameters.map((parameter) => parameter.type).concat(method.returnType ? [method.returnType] : [])
.flatMap((method) => );
method.parameters.map((parameter) => parameter.type).concat(method.returnType ? [method.returnType] : [])
);
} }
function recursiveVisitNamedType( function recursiveVisitNamedType(
valueType: ValueType, valueType: ValueType,
visit: (namedType: NamedType, path: string) => void, visit: (namedType: CustomType | EnumType, path: string) => void,
path = '' path = ''
): void { ): void {
if (isCustomType(valueType)) { if (isCustomType(valueType)) {

Просмотреть файл

@ -2,27 +2,33 @@ import yargs from 'yargs';
import { CodeGenerator, RenderingLanguage } from './generator/CodeGenerator'; import { CodeGenerator, RenderingLanguage } from './generator/CodeGenerator';
import { parseKeyValueText } from './utils'; import { parseKeyValueText } from './utils';
interface Config {
moduleGenerationMaps: { interfacePaths: string[]; moduleTemplatePath: string; outputDirectory: string }[];
namedTypesTemplatePath: string;
namedTypesOutputPath: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultCustomTags?: any;
dropInterfaceIPrefix?: boolean;
}
const program = yargs(process.argv.slice(2)); const program = yargs(process.argv.slice(2));
const args = program const args = program
.config()
.options({ .options({
interfacePaths: { interfacePaths: {
type: 'string', type: 'string',
array: true, array: true,
demandOption: true,
describe: 'The path of api interface which should extend IExportedApi', describe: 'The path of api interface which should extend IExportedApi',
}, },
outputDirectory: { outputDirectory: {
type: 'string', type: 'string',
demandOption: true,
describe: 'The path of output directory', describe: 'The path of output directory',
}, },
moduleTemplatePath: { moduleTemplatePath: {
type: 'string', type: 'string',
demandOption: true,
describe: 'The path of module template', describe: 'The path of module template',
}, },
namedTypesTemplate: { namedTypesTemplatePath: {
type: 'string', type: 'string',
demandOption: true, demandOption: true,
describe: 'The path of named types template', describe: 'The path of named types template',
@ -30,7 +36,7 @@ const args = program
defaultCustomTag: { defaultCustomTag: {
type: 'string', type: 'string',
array: true, array: true,
coerce: (values: string[]) => values.map(tagString => parseKeyValueText(tagString)), coerce: (values: string[]) => values.map((tagString) => parseKeyValueText(tagString)),
default: [], default: [],
describe: 'Default values for custom tags', describe: 'Default values for custom tags',
}, },
@ -43,20 +49,39 @@ const args = program
.help().argv; .help().argv;
function run(): void { function run(): void {
const config = args as unknown as Config;
const generator = new CodeGenerator(); const generator = new CodeGenerator();
generator.parse({ config.moduleGenerationMaps.forEach((moduleGenerationMap, index) => {
tag: 'APIs', generator.parse({
interfacePaths: args.interfacePaths, interfacePaths: moduleGenerationMap.interfacePaths,
defaultCustomTags: Object.fromEntries(args.defaultCustomTag.map(tag => [tag.key, tag.value])), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
dropInterfaceIPrefix: args.dropInterfaceIPrefix, defaultCustomTags: config.defaultCustomTags ?? {},
dropInterfaceIPrefix: config.dropInterfaceIPrefix ?? false,
});
generator.printModules(index);
}); });
generator.printModules({ tag: 'APIs' });
generator.render({ generator.parseNamedTypes();
tag: 'APIs', generator.printSharedNamedTypes();
config.moduleGenerationMaps.forEach((_, index) => {
generator.printModules(index);
});
config.moduleGenerationMaps.forEach((moduleGenerationMap, index) => {
generator.renderModules({
index,
language: RenderingLanguage.Swift,
outputDirectory: moduleGenerationMap.outputDirectory,
moduleTemplatePath: moduleGenerationMap.moduleTemplatePath,
});
});
generator.renderNamedTypes({
language: RenderingLanguage.Swift, language: RenderingLanguage.Swift,
outputDirectory: args.outputDirectory, namedTypesTemplatePath: config.namedTypesTemplatePath,
moduleTemplatePath: args.moduleTemplatePath, namedTypesOutputPath: config.namedTypesOutputPath,
namedTypesTemplatePath: args.namedTypesTemplate,
}); });
} }

Просмотреть файл

@ -28,7 +28,11 @@ export class Parser {
parse(): Module[] { parse(): Module[] {
const modules: Module[] = []; const modules: Module[] = [];
this.program.getSourceFiles().forEach((sourceFile) => { this.program.getRootFileNames().forEach((fileName) => {
const sourceFile = this.program.getSourceFile(fileName);
if (sourceFile === undefined) {
throw Error('Source file not found');
}
ts.forEachChild(sourceFile, (node) => { ts.forEachChild(sourceFile, (node) => {
const module = this.moduleFromNode(node); const module = this.moduleFromNode(node);
if (module !== null) { if (module !== null) {

Просмотреть файл

@ -1,7 +1,12 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path';
import Mustache from 'mustache'; import Mustache from 'mustache';
export function renderCode<View>(templatePath: string, view: View): string { export function renderCode<View>(templatePath: string, view: View): string {
const template = fs.readFileSync(templatePath).toString(); const template = fs.readFileSync(templatePath).toString();
return Mustache.render(template, view); const directory = path.dirname(templatePath);
return Mustache.render(template, view, (partialName) => {
const partialPath = path.join(directory, `${partialName}.mustache`);
return fs.readFileSync(partialPath).toString();
});
} }

Просмотреть файл

@ -1,8 +1,13 @@
import { uncapitalize } from '../../utils';
import { EnumSubType, EnumType } from '../../types'; import { EnumSubType, EnumType } from '../../types';
import { EnumTypeView } from '../views'; import { EnumTypeView } from '../views';
export class SwiftEnumTypeView implements EnumTypeView { export class SwiftEnumTypeView implements EnumTypeView {
constructor(readonly typeName: string, private enumType: EnumType) {} constructor(private enumType: EnumType) {}
get typeName(): string {
return this.enumType.name;
}
get valueType(): string { get valueType(): string {
switch (this.enumType.subType) { switch (this.enumType.subType) {
@ -17,7 +22,8 @@ export class SwiftEnumTypeView implements EnumTypeView {
get members(): { key: string; value: string }[] { get members(): { key: string; value: string }[] {
return Object.entries(this.enumType.members).map(([key, value]) => ({ return Object.entries(this.enumType.members).map(([key, value]) => ({
key, // TODO: Convert to camel case instead of uncapitalize
key: uncapitalize(key),
value: typeof value === 'string' ? `"${value}"` : `${value}`, value: typeof value === 'string' ? `"${value}"` : `${value}`,
})); }));
} }

Просмотреть файл

@ -1,9 +1,9 @@
import { Module } from '../../types'; import { Module } from '../../types';
import { ModuleView, MethodView } from '../views'; import { ModuleView, MethodView, NamedTypeView } from '../views';
import { SwiftMethodView } from './SwiftMethodView'; import { SwiftMethodView } from './SwiftMethodView';
export class SwiftModuleView implements ModuleView { export class SwiftModuleView implements ModuleView {
constructor(private module: Module) {} constructor(private readonly module: Module, readonly associatedTypes: NamedTypeView[]) {}
get moduleName(): string { get moduleName(): string {
return this.module.name; return this.module.name;

Просмотреть файл

@ -9,13 +9,11 @@ export interface MethodView {
export interface ModuleView { export interface ModuleView {
readonly moduleName: string; readonly moduleName: string;
readonly methods: MethodView[]; readonly methods: MethodView[];
readonly associatedTypes: NamedTypeView[];
readonly customTags: Record<string, string>; readonly customTags: Record<string, string>;
} }
export interface NamedTypesView { export type NamedTypeView = (CustomTypeView | EnumTypeView) & { custom?: boolean; enum?: boolean };
readonly customTypes: CustomTypeView[];
readonly enumTypes: EnumTypeView[];
}
export interface CustomTypeView { export interface CustomTypeView {
readonly typeName: string; readonly typeName: string;

Просмотреть файл

@ -18,7 +18,8 @@ const typeColor = chalk.yellow;
const valueColor = chalk.cyan; const valueColor = chalk.cyan;
const documentationColor = chalk.gray; const documentationColor = chalk.gray;
export function serializeModule(module: Module): string { export function serializeModule(module: Module, associatedTypes: NamedType[]): string {
const serializedAssociatedTypes = associatedTypes.map((associatedType) => serializeNamedType(associatedType));
const customTags = const customTags =
Object.keys(module.customTags).length > 0 ? `Custom tags: ${JSON.stringify(module.customTags)}\n` : ''; Object.keys(module.customTags).length > 0 ? `Custom tags: ${JSON.stringify(module.customTags)}\n` : '';
@ -32,13 +33,21 @@ ${module.methods
.map((line) => ` ${line}`) .map((line) => ` ${line}`)
.join('\n') .join('\n')
) )
.join('\n')} .join('\n')}${
serializedAssociatedTypes.length > 0
? `\n\n${serializedAssociatedTypes
.join('\n')
.split('\n')
.map((line) => ` ${line}`)
.join('\n')}`
: ''
}
}`; }`;
} }
export function serializeNamedType(typeName: string, namedType: NamedType): string { export function serializeNamedType(namedType: NamedType): string {
if (isCustomType(namedType)) { if (isCustomType(namedType)) {
return `${keywordColor('Type')} ${typeName} { return `${keywordColor('Type')} ${namedType.name} {
${namedType.members ${namedType.members
.map( .map(
(member) => (member) =>
@ -48,7 +57,7 @@ ${namedType.members
}`; }`;
} }
return `${keywordColor('Enum')} ${typeName} { return `${keywordColor('Enum')} ${namedType.name} {
${Object.entries(namedType.members) ${Object.entries(namedType.members)
.map(([key, value]) => ` ${identifierColor(key)} = ${valueColor(value)}`) .map(([key, value]) => ` ${identifierColor(key)} = ${valueColor(value)}`)
.join('\n')} .join('\n')}

Просмотреть файл

@ -6,8 +6,16 @@ export function capitalize(text: string): string {
return text[0].toUpperCase() + text.slice(1); return text[0].toUpperCase() + text.slice(1);
} }
export function uncapitalize(text: string): string {
if (text.length === 0) {
return text;
}
return text[0].toLowerCase() + text.slice(1);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function parseKeyValueText(text: string): { key: string, value: any } { export function parseKeyValueText(text: string): [string, any] {
const index = text.indexOf('='); const index = text.indexOf('=');
if (index === -1) { if (index === -1) {
throw Error('Invalid custom tag'); throw Error('Invalid custom tag');
@ -24,5 +32,5 @@ export function parseKeyValueText(text: string): { key: string, value: any } {
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
return { key, value }; return [key, value];
} }

Просмотреть файл

@ -24,7 +24,7 @@ describe('Parser', () => {
`; `;
withTempParser(exportedTrueSourceCode, parser => { withTempParser(exportedTrueSourceCode, parser => {
const modules = parser.parse(); const modules = parser.parse();
expect(modules).to.deep.equal([{name: 'ExportTrueInterface', methods: [], documentation: ''}]); expect(modules).to.deep.equal([{name: 'ExportTrueInterface', methods: [], documentation: '', customTags: {}}]);
}); });
}); });
@ -52,7 +52,8 @@ describe('Parser', () => {
returnType: null, returnType: null,
documentation: 'This is an example documentation for the method', documentation: 'This is an example documentation for the method',
}], }],
documentation: 'This is an example documentation for the module' documentation: 'This is an example documentation for the module',
customTags: {},
}]); }]);
}); });
}); });
@ -71,7 +72,7 @@ describe('Parser', () => {
const stubWarn = sinon.stub(console, 'warn'); const stubWarn = sinon.stub(console, 'warn');
const modules = parser.parse(); const modules = parser.parse();
expect(modules).to.deep.equal([{name: 'MockedInterface', methods: [], documentation: ''}]); expect(modules).to.deep.equal([{name: 'MockedInterface', methods: [], documentation: '', customTags: {}}]);
const expectedWarning = warnMessage(`Skipped "invalidProperty: string;" at ${filePath}:5 because it is not valid method signature. Please define only methods.`); const expectedWarning = warnMessage(`Skipped "invalidProperty: string;" at ${filePath}:5 because it is not valid method signature. Please define only methods.`);
expect(stubWarn).to.have.been.calledWith(expectedWarning); expect(stubWarn).to.have.been.calledWith(expectedWarning);
@ -94,7 +95,7 @@ describe('Parser', () => {
const stubWarn = sinon.stub(console, 'warn'); const stubWarn = sinon.stub(console, 'warn');
const modules = parser.parse(); const modules = parser.parse();
expect(modules).to.deep.equal([{name: 'MockedInterface', methods: [], documentation: ''}]); expect(modules).to.deep.equal([{name: 'MockedInterface', methods: [], documentation: '', customTags: {}}]);
const expectedWarning = warnMessage(`Skipped "multipleParamsMethod(foo: string, bar: number);" at ${filePath}:5 because it has multiple parameters. Methods should only have one property. Please use destructuring object for multiple parameters.`); const expectedWarning = warnMessage(`Skipped "multipleParamsMethod(foo: string, bar: number);" at ${filePath}:5 because it has multiple parameters. Methods should only have one property. Please use destructuring object for multiple parameters.`);
expect(stubWarn).to.have.been.calledWith(expectedWarning); expect(stubWarn).to.have.been.calledWith(expectedWarning);

Просмотреть файл

@ -1,5 +1,5 @@
{ {
"include": ["src/*.ts", "src/**/*.ts", "test/*.ts", "test/**/*.ts"], "include": ["src/*.ts", "src/**/*.ts"],
"compilerOptions": { "compilerOptions": {
"resolveJsonModule": true, "resolveJsonModule": true,
"target": "es6", "target": "es6",

3727
yarn.lock

Разница между файлами не показана из-за своего большого размера Загрузить разницу