Skip to content

Commit

Permalink
refactor: allow import internal types
Browse files Browse the repository at this point in the history
  • Loading branch information
askuzminov committed Jan 19, 2021
1 parent a1b75a6 commit 7837186
Show file tree
Hide file tree
Showing 14 changed files with 381 additions and 329 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ module.exports = {
'undefined',
],
'id-match': 'error',
'import/export': 'off',
'import/order': [
'error',
{
Expand Down
14 changes: 10 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,23 +232,29 @@ import { grpc } from './grpc';
// You not need use catch for more flatten code without try {}
const user = await grpc(whisk_api_user_v2_UserAPI_GetMe);

console.log(user.success && user.data.user?.email);
console.log(!user.success && user.error.message);

if (user.success) {
// If success we have typed data
// You don't need do any extra check
console.log(user.data.user?.email);
} else {
// If error we have same structure with optional fields
console.log(
res.error.message, // string?
res.error.data, // unknown? | Object - lib tries do safe JSON.parse
res.error.grpcCode, // number?
res.error.httpStatus // number?
user.error.message, // string?
user.error.data, // unknown? | Object - lib tries do safe JSON.parse
user.error.grpcCode, // number?
user.error.httpStatus // number?
);
}

// Destruction example for flatten style
const { data: me, error } = await grpc(whisk_api_user_v2_UserAPI_GetMe);

console.log(me?.user?.email);
console.log(error?.message);

if (me) {
console.log(me.user?.email);
}
Expand Down
2 changes: 1 addition & 1 deletion src/generator/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { isText } from '@whisklabs/typeguards';
import { existsSync } from 'fs';
import { join } from 'path';

import { generator } from '.';
import { generator } from './main';

const {
PROTO_DIR,
Expand Down
10 changes: 5 additions & 5 deletions src/generator/enum.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { isNumber } from '@whisklabs/typeguards';

import { Enum } from '../parser/types';
import { Parser } from '../parser';
import { EnumsList, MakeOuts } from './generator';
import { checkDublicate, checkSame, safeString } from './utils';

export function enums(pack: string, out: MakeOuts, items: Enum[], enumsList: EnumsList) {
export function enums(pack: string, out: MakeOuts, items: Parser.Enum[], enumsList: EnumsList) {
for (const msg of items) {
enu(`${pack}_${msg.name}`, out, msg, enumsList);
}
}

function enu(pack: string, out: MakeOuts, item: Enum, enumsList: EnumsList) {
function enu(pack: string, out: MakeOuts, item: Parser.Enum, enumsList: EnumsList) {
const eName = safeString(pack);
enumsList.add(eName);

Expand All @@ -23,7 +23,7 @@ function enu(pack: string, out: MakeOuts, item: Enum, enumsList: EnumsList) {

for (const field in item.values) {
const val = item.values[field].value;
if (isNumber(val) && !isNaN(val) && !isInvalid(field, val)) {
if (isNumber(val) && !isNaN(val) && !isInvalidEnumField(field, val)) {
cID(val, `${pack}.${field}`);
cName(field, `${pack}.${field}`);
out.js.push(` ${field}: ${val},`);
Expand All @@ -36,5 +36,5 @@ function enu(pack: string, out: MakeOuts, item: Enum, enumsList: EnumsList) {
out.dts.push(`export type ${eName} = Values<typeof ${eName}>;`);
}

const isInvalid = (field: string, val: number) =>
const isInvalidEnumField = (field: string, val: number) =>
val === 0 && (field.endsWith('_UNSPECIFIED') || field.endsWith('_INVALID'));
27 changes: 17 additions & 10 deletions src/generator/field.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { isPresent, isString } from '@whisklabs/typeguards';

import { Field } from '../parser/types';
import { Parser } from '../parser';
import { GOOGLE_WRAPPERS, TYPES } from './constants';
import { EnumsList, MakeOuts } from './generator';
import { safeString } from './utils';

export const getField = (field: Field, base: string, enumsList: EnumsList, name: string, out: MakeOuts): string => {
export const getField = (
field: Parser.Field,
base: string,
enumsList: EnumsList,
name: string,
out: MakeOuts
): string => {
const type = field.type;

if (type === 'map') {
const to = getField({ type: field.map?.to } as Field, base, enumsList, name, out);
const from = getField({ type: field.map?.from } as Field, base, enumsList, name, out);
const to = getField({ type: field.map?.to } as Parser.Field, base, enumsList, name, out);
const from = getField({ type: field.map?.from } as Parser.Field, base, enumsList, name, out);
return `Record<${from}, ${to}>`;
} else if (isString(TYPES[type])) {
return TYPES[type];
Expand All @@ -23,15 +29,15 @@ export const getField = (field: Field, base: string, enumsList: EnumsList, name:
}
};

export const getStruct = (field: Field, base: string, enumsList: EnumsList): string => {
export const getStruct = (field: Parser.Field, base: string, enumsList: EnumsList): string => {
const type = field.type;

if (type === 'map') {
const from = getStruct({ type: field.map?.from } as Field, base, enumsList);
const to = getStruct({ type: field.map?.to } as Field, base, enumsList);
const from = getStruct({ type: field.map?.from } as Parser.Field, base, enumsList);
const to = getStruct({ type: field.map?.to } as Parser.Field, base, enumsList);
return `["map", ${from}, ${to}]`;
} else if (field.repeated) {
return `["repeated", ${getStruct({ type } as Field, base, enumsList)}]`;
return `["repeated", ${getStruct({ type } as Parser.Field, base, enumsList)}]`;
} else if (isString(TYPES[type])) {
return `"${type}"`;
} else if (isString(GOOGLE_WRAPPERS[type])) {
Expand All @@ -45,11 +51,12 @@ export const getStruct = (field: Field, base: string, enumsList: EnumsList): str
export const pathField = (field: string, base: string) =>
/\./.test(field) ? safeString(field) : safeString(`${base}_${field}`);

export const isRequired = (field: Field) =>
export const isRequiredField = (field: Parser.Field) =>
isPresent(field.options.required)
? field.options.required !== false
: field.optional
? false
: field.required || field.repeated || field.type === 'map' || isString(TYPES[field.type]);

export const isEnum = (field: Field, base: string, enumsList: EnumsList) => enumsList.has(pathField(field.type, base));
export const isEnumField = (field: Parser.Field, base: string, enumsList: EnumsList) =>
enumsList.has(pathField(field.type, base));
4 changes: 2 additions & 2 deletions src/generator/generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Schema } from '../parser/types';
import { Parser } from '../parser';

export interface Config {
dir: string;
Expand All @@ -14,7 +14,7 @@ export interface Config {

export interface Out {
filepaths: string[];
schemas: Schema[];
schemas: Parser.Schema[];
}

export type MakeOut = string | null;
Expand Down
190 changes: 8 additions & 182 deletions src/generator/index.ts
Original file line number Diff line number Diff line change
@@ -1,182 +1,8 @@
/* eslint-disable no-console */

import { isText } from '@whisklabs/typeguards';
import { promises as fs } from 'fs';
import { extname, isAbsolute, join, parse as pathParse, relative } from 'path';
import { CompilerOptions, ModuleKind, ModuleResolutionKind, ScriptTarget, transpileModule } from 'typescript';

import { parser } from '../parser';
import { Schema } from '../parser/types';
import { enums } from './enum';
import { Config, EnumsList, MakeOuts, Out } from './generator';
import { messages } from './message';
import { services } from './service';

export async function generator({
dir,
out,
exclude = /^$/,
version = 'unknown',
debug = false,
packageName,
packageVersion,
packageUrl = 'git@github.com:whisklabs/npm.git',
packageRegistry = 'https://npm.pkg.github.com/',
}: Config) {
const IN_DIR = isAbsolute(dir) ? dir : join(process.cwd(), dir);
const OUT_DIR = isAbsolute(out) ? out : join(process.cwd(), out);
const DEBUG_DIR = join(OUT_DIR, 'debug');

const lic = `// Code created by grpc-generator. Version: ${version}`;
const lib = `import {
FieldMap,
FieldRepeated,
FieldType,
FieldItem,
Field,
FieldEmpty,
FieldGet,
Service,
ServiceRequest,
ServiceResponse,
Values
} from '${version === 'test' ? '../../src' : '@whisklabs/grpc'}';`;
let lastFile = '';

async function walk(directory: string, result: Out = { filepaths: [], schemas: [] }) {
const dirFiles = await fs.readdir(directory);
for (const filename of dirFiles) {
const filepath = join(directory, filename);
lastFile = filepath;
if ((await fs.stat(filepath)).isDirectory()) {
await walk(filepath, result);
} else if (extname(filename) === '.proto' && !exclude.test(filepath)) {
result.filepaths.push(filepath);

const parsed = parser(await fs.readFile(filepath, 'utf8'));
if (debug) {
const outFile = pathParse(join(DEBUG_DIR, relative(dir ?? '', filepath)));

await fs.mkdir(outFile.dir, { recursive: true });
await fs.writeFile(join(outFile.dir, `${outFile.name}.json`), JSON.stringify(parsed, null, 2));
}
result.schemas.push(parsed);
}
}
return result;
}

return walk(IN_DIR)
.then(async ({ schemas }) => {
const { js, dts, errors, fields, names } = make(schemas);

for (const [type, field] of fields) {
if (!names.has(type)) {
errors.push(`\x1b[31mNo found message or enum:\x1b[0m ${type} [${field}]`);
}
}

dts.unshift(lic, lib, '');
js.unshift(lic, '"use strict";', '');

const dtsString = dts.join('\n');
const jsString = js.join('\n');

checkTypes(dtsString, errors);
checkTypes(jsString, errors);

const hasError = errors.length > 0;

if (hasError) {
errors.forEach(i => console.error(i));
} else {
await fs.mkdir(OUT_DIR, { recursive: true });
await fs.writeFile(join(OUT_DIR, 'index.d.ts'), dtsString);
await fs.writeFile(join(OUT_DIR, 'index.js'), transpile(jsString));
await fs.writeFile(join(OUT_DIR, 'esm.js'), transpile(jsString, { module: ModuleKind.ES2015 }));
if (isText(packageName) && isText(packageVersion)) {
await fs.writeFile(
join(OUT_DIR, 'package.json'),
packageTemplate({ packageName, packageVersion, packageUrl, packageRegistry })
);
}
}

return hasError;
})
.catch(e => {
console.info(`Last processing file was "\x1b[31m${lastFile}\x1b[0m"`);
console.error(e);
return true;
});
}

const transpile = (source: string, options?: CompilerOptions) =>
transpileModule(source, {
compilerOptions: {
allowJs: true,
module: ModuleKind.CommonJS,
target: ScriptTarget.ES5,
moduleResolution: ModuleResolutionKind.NodeJs,
...options,
},
}).outputText;

function checkTypes(file: string, errors: string[]) {
const { diagnostics } = transpileModule(file, { reportDiagnostics: true });

for (const diagnostic of diagnostics ?? []) {
errors.push(`\x1b[31mTS error:\x1b[0m ${JSON.stringify(diagnostic.messageText)}`);
}
}

function make(schemas: Schema[]): MakeOuts {
const enumsList: EnumsList = new Set();

const out: MakeOuts = {
js: [],
dts: [],
names: new Set(),
errors: [],
fields: [],
};

for (const schema of schemas) {
const path = schema.package ?? '';
enums(path, out, schema.enums, enumsList);
}

for (const schema of schemas) {
const path = schema.package ?? '';
messages(path, out, schema.messages, [], enumsList);

services(path, out, schema.services);
}

return out;
}

interface Package {
packageName: string;
packageVersion: string;
packageUrl: string;
packageRegistry: string;
}

const packageTemplate = ({ packageName, packageVersion, packageUrl, packageRegistry }: Package) => `{
"name": "${packageName}",
"version": "${packageVersion}",
"repository": {
"type": "git",
"url": "${packageUrl}"
},
"publishConfig": {
"registry": "${packageRegistry}"
},
"main": "./index.js",
"module": "./esm.js",
"types": "./index.d.ts",
"peerDependencies": {
"@whisklabs/grpc": "*"
}
}`;
export * from './main';
export * from './utils';
export * from './generator';
export * from './constants';
export * from './enum';
export * from './field';
export * from './message';
export * from './service';

0 comments on commit 7837186

Please sign in to comment.