Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: introduce standardized error messages and stacktraces #869

Merged
merged 5 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/openapi-generator/src/apiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { resolveLiteralOrIdentifier } from './resolveInit';
import { parseRoute, type Route } from './route';
import { SourceFile } from './sourceFile';
import { OpenAPIV3 } from 'openapi-types';
import { errorLeft } from './error';

export function parseApiSpec(
project: Project,
sourceFile: SourceFile,
expr: swc.Expression,
): E.Either<string, Route[]> {
if (expr.type !== 'ObjectExpression') {
return E.left(`unimplemented route expression type ${expr.type}`);
return errorLeft(`unimplemented route expression type ${expr.type}`);
}

const result: Route[] = [];
Expand All @@ -34,7 +35,7 @@ export function parseApiSpec(
if (spreadExpr.type === 'CallExpression') {
const arg = spreadExpr.arguments[0];
if (arg === undefined) {
return E.left(`unimplemented spread argument type ${arg}`);
return errorLeft(`unimplemented spread argument type ${arg}`);
}
spreadExpr = arg.expression;
}
Expand All @@ -47,7 +48,7 @@ export function parseApiSpec(
}

if (apiAction.type !== 'KeyValueProperty') {
return E.left(`unimplemented route property type ${apiAction.type}`);
return errorLeft(`unimplemented route property type ${apiAction.type}`);
}
const routes = apiAction.value;
const routesInitE = resolveLiteralOrIdentifier(project, sourceFile, routes);
Expand All @@ -56,11 +57,11 @@ export function parseApiSpec(
}
const [routesSource, routesInit] = routesInitE.right;
if (routesInit.type !== 'ObjectExpression') {
return E.left(`unimplemented routes type ${routes.type}`);
return errorLeft(`unimplemented routes type ${routes.type}`);
}
for (const route of Object.values(routesInit.properties)) {
if (route.type !== 'KeyValueProperty') {
return E.left(`unimplemented route type ${route.type}`);
return errorLeft(`unimplemented route type ${route.type}`);
}
const routeExpr = route.value;
const routeInitE = resolveLiteralOrIdentifier(project, routesSource, routeExpr);
Expand Down
20 changes: 10 additions & 10 deletions packages/openapi-generator/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const app = command({
const codecFilePath = p.resolve(codecFile);
const codecModule = await import(codecFilePath);
if (codecModule.default === undefined) {
console.error(`Could not find default export in ${codecFilePath}`);
console.error(`[ERROR] Could not find default export in ${codecFilePath}`);
anshchaturvedi marked this conversation as resolved.
Show resolved Hide resolved
process.exit(1);
}
const customCodecs = codecModule.default(E);
Expand All @@ -96,13 +96,13 @@ const app = command({

const project = await new Project({}, knownImports).parseEntryPoint(filePath);
if (E.isLeft(project)) {
console.error(project.left);
console.error(`[ERROR] ${project.left}`);
process.exit(1);
}

const entryPoint = project.right.get(filePath);
if (entryPoint === undefined) {
console.error(`Could not find entry point ${filePath}`);
console.error(`[ERROR] Could not find entry point ${filePath}`);
process.exit(1);
}

Expand All @@ -120,21 +120,21 @@ const app = command({
symbol.init.callee.type === 'Import'
) {
console.error(
`Skipping ${symbol.name} because it is a ${symbol.init.callee.type}`,
`[WARN] Skipping ${symbol.name} because it is a ${symbol.init.callee.type}`,
anshchaturvedi marked this conversation as resolved.
Show resolved Hide resolved
);
continue;
} else if (!isApiSpec(entryPoint, symbol.init.callee)) {
continue;
}
console.error(`Found API spec in ${symbol.name}`);
console.error(`[INFO] Found API spec in ${symbol.name}`);
anshchaturvedi marked this conversation as resolved.
Show resolved Hide resolved

const result = parseApiSpec(
project.right,
entryPoint,
symbol.init.arguments[0]!.expression,
);
if (E.isLeft(result)) {
console.error(`Error parsing ${symbol.name}: ${result.left}`);
console.error(`[ERROR] Error when parsing ${symbol.name}: ${result.left}`);
process.exit(1);
}

Expand All @@ -145,7 +145,7 @@ const app = command({
apiSpec.push(...result.right);
}
if (apiSpec.length === 0) {
console.error(`Could not find API spec in ${filePath}`);
console.error(`[ERROR] Could not find API spec in ${filePath}`);
process.exit(1);
}

Expand All @@ -166,14 +166,14 @@ const app = command({
}
const sourceFile = project.right.get(ref.location);
if (sourceFile === undefined) {
console.error(`Could not find '${ref.name}' from '${ref.location}'`);
console.error(`[ERROR] Could not find '${ref.name}' from '${ref.location}'`);
process.exit(1);
}

const initE = findSymbolInitializer(project.right, sourceFile, ref.name);
if (E.isLeft(initE)) {
console.error(
`Could not find symbol '${ref.name}' in '${ref.location}': ${initE.left}`,
`[ERROR] Could not find symbol '${ref.name}' in '${ref.location}': ${initE.left}`,
);
process.exit(1);
}
Expand All @@ -182,7 +182,7 @@ const app = command({
const codecE = parseCodecInitializer(project.right, newSourceFile, init);
if (E.isLeft(codecE)) {
console.error(
`Could not parse codec '${ref.name}' in '${ref.location}': ${codecE.left}`,
`[ERROR] Could not parse codec '${ref.name}' in '${ref.location}': ${codecE.left}`,
);
process.exit(1);
}
Expand Down
47 changes: 25 additions & 22 deletions packages/openapi-generator/src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { findSymbolInitializer } from './resolveInit';
import type { SourceFile } from './sourceFile';

import type { KnownCodec } from './knownImports';
import { errorLeft } from './error';

type ResolvedIdentifier = Schema | { type: 'codec'; schema: KnownCodec };

Expand All @@ -26,9 +27,9 @@ function codecIdentifier(

const imp = source.symbols.imports.find((s) => s.localName === id.value);
if (imp === undefined) {
return E.left(`Unknown identifier ${id.value}`);
return errorLeft(`Unknown identifier ${id.value}`);
} else if (imp.type === 'star') {
return E.left(`Tried to use star import as codec ${id.value}`);
return errorLeft(`Tried to use star import as codec ${id.value}`);
}
const knownImport = project.resolveKnownImport(imp.from, imp.importedName);
if (knownImport !== undefined) {
Expand All @@ -54,10 +55,12 @@ function codecIdentifier(
const object = id.object;
if (object.type !== 'Identifier') {
if (object.type === 'MemberExpression')
return E.left(
`Object ${((object as swc.MemberExpression) && { value: String }).value} is deeply nested, which is unsupported`,
return errorLeft(
`Object ${
((object as swc.MemberExpression) && { value: String }).value
} is deeply nested, which is unsupported`,
);
return E.left(`Unimplemented object type ${object.type}`);
return errorLeft(`Unimplemented object type ${object.type}`);
}

// Parse member expressions that come from `* as foo` imports
Expand All @@ -66,7 +69,7 @@ function codecIdentifier(
);
if (starImportSym !== undefined) {
if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
return errorLeft(`Unimplemented property type ${id.property.type}`);
}

const name = id.property.value;
Expand Down Expand Up @@ -96,7 +99,7 @@ function codecIdentifier(
);
if (objectImportSym !== undefined) {
if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
return errorLeft(`Unimplemented property type ${id.property.type}`);
}
const name = id.property.value;

Expand All @@ -113,9 +116,9 @@ function codecIdentifier(
if (E.isLeft(objectSchemaE)) {
return objectSchemaE;
} else if (objectSchemaE.right.type !== 'object') {
return E.left(`Expected object, got '${objectSchemaE.right.type}'`);
return errorLeft(`Expected object, got '${objectSchemaE.right.type}'`);
} else if (objectSchemaE.right.properties[name] === undefined) {
return E.left(
return errorLeft(
`Unknown property '${name}' in '${objectImportSym.localName}' from '${objectImportSym.from}'`,
);
} else {
Expand All @@ -124,7 +127,7 @@ function codecIdentifier(
}

if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
return errorLeft(`Unimplemented property type ${id.property.type}`);
}

// Parse locally declared member expressions
Expand All @@ -136,11 +139,11 @@ function codecIdentifier(
if (E.isLeft(schemaE)) {
return schemaE;
} else if (schemaE.right.type !== 'object') {
return E.left(
return errorLeft(
`Expected object, got '${schemaE.right.type}' for '${declarationSym.name}'`,
);
} else if (schemaE.right.properties[id.property.value] === undefined) {
return E.left(
return errorLeft(
`Unknown property '${id.property.value}' in '${declarationSym.name}'`,
);
} else {
Expand All @@ -158,7 +161,7 @@ function codecIdentifier(
}
}

return E.left(`Unimplemented identifier type ${id.type}`);
return errorLeft(`Unimplemented identifier type ${id.type}`);
}

function parseObjectExpression(
Expand Down Expand Up @@ -210,19 +213,19 @@ function parseObjectExpression(
schema = schemaE.right;
}
if (schema.type !== 'object') {
return E.left(`Spread element must be object`);
return errorLeft(`Spread element must be object`);
}
Object.assign(result.properties, schema.properties);
result.required.push(...schema.required);
continue;
} else if (property.type !== 'KeyValueProperty') {
return E.left(`Unimplemented property type ${property.type}`);
return errorLeft(`Unimplemented property type ${property.type}`);
} else if (
property.key.type !== 'Identifier' &&
property.key.type !== 'StringLiteral' &&
property.key.type !== 'NumericLiteral'
) {
return E.left(`Unimplemented property key type ${property.key.type}`);
return errorLeft(`Unimplemented property key type ${property.key.type}`);
}
const commentEndIdx = property.key.span.start;
const comments = leadingComment(
Expand Down Expand Up @@ -254,7 +257,7 @@ function parseArrayExpression(
const result: Schema[] = [];
for (const element of array.elements) {
if (element === undefined) {
return E.left('Undefined array element');
return errorLeft('Undefined array element');
}
const valueE = parsePlainInitializer(project, source, element.expression);
if (E.isLeft(valueE)) {
Expand All @@ -279,7 +282,7 @@ function parseArrayExpression(
init = schemaE.right;
}
if (init.type !== 'tuple') {
return E.left('Spread element must be array literal');
return errorLeft('Spread element must be array literal');
}
result.push(...init.schemas);
} else {
Expand Down Expand Up @@ -342,7 +345,7 @@ export function parseCodecInitializer(
} else if (init.type === 'CallExpression') {
const callee = init.callee;
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') {
return E.left(`Unimplemented callee type ${init.callee.type}`);
return errorLeft(`Unimplemented callee type ${init.callee.type}`);
}
const identifierE = codecIdentifier(project, source, callee);
if (E.isLeft(identifierE)) {
Expand All @@ -364,10 +367,10 @@ export function parseCodecInitializer(
// schema.location might be a package name -> need to resolve the path from the project types
const path = project.getTypes()[schema.name];
if (path === undefined)
return E.left(`Cannot find module '${schema.location}' in the project`);
return errorLeft(`Cannot find module '${schema.location}' in the project`);
refSource = project.get(path);
if (refSource === undefined) {
return E.left(`Cannot find '${schema.name}' from '${schema.location}'`);
return errorLeft(`Cannot find '${schema.name}' from '${schema.location}'`);
}
}
const initE = findSymbolInitializer(project, refSource, schema.name);
Expand All @@ -394,6 +397,6 @@ export function parseCodecInitializer(
E.chain((args) => identifier.schema(deref, ...args)),
);
} else {
return E.left(`Unimplemented initializer type ${init.type}`);
return errorLeft(`Unimplemented initializer type ${init.type}`);
}
}
22 changes: 22 additions & 0 deletions packages/openapi-generator/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as E from 'fp-ts/Either';

/**
* A wrapper around `E.left` that includes a stacktrace.
* @param message the error message
* @returns an `E.left` with the error message and a stacktrace
*/
export function errorLeft(message: string): E.Either<string, never> {
const stacktrace = new Error().stack!.split('\n').slice(2).join('\n');
const messageWithStacktrace = message + '\n' + stacktrace;

return E.left(messageWithStacktrace);
}

/**
* Testing utility to strip the stacktrace from errors.
* @param errors the list of errors to strip
* @returns the errors without the stacktrace
*/
export function stripStacktraceOfErrors(errors: string[]) {
return errors.map((e) => e!.split('\n')[0]);
}
Loading