Skip to content

Commit

Permalink
feat: add errorLeft wrapper
Browse files Browse the repository at this point in the history
This adds a stacktrace as a string alongside the message passed to the E.left.

Ticket: DX-660
  • Loading branch information
anshchaturvedi committed Aug 7, 2024
1 parent 44738c9 commit b7c83a6
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 69 deletions.
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
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}`);
}
}
13 changes: 13 additions & 0 deletions packages/openapi-generator/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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 errorLeft(messageWithStacktrace);
}
31 changes: 16 additions & 15 deletions packages/openapi-generator/src/knownImports.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as E from 'fp-ts/Either';

import { isPrimitive, type Schema } from './ir';
import { errorLeft } from './error';

export type DerefFn = (ref: Schema) => E.Either<string, Schema>;
export type KnownCodec = (
Expand Down Expand Up @@ -28,13 +29,13 @@ export const KNOWN_IMPORTS: KnownImports = {
global: {
'Object.assign': (_, ...schemas) => {
if (schemas.length < 2) {
return E.left('assign must have at least 2 arguments');
return errorLeft('assign must have at least 2 arguments');
}
const [target, ...sources] = schemas;
if (target === undefined) {
return E.left('assign target must be object');
return errorLeft('assign target must be object');
} else if (target.type !== 'object') {
return E.left('assign target must be object');
return errorLeft('assign target must be object');
}
const properties = sources.reduce((acc, source) => {
if (source.type !== 'object') {
Expand All @@ -60,7 +61,7 @@ export const KNOWN_IMPORTS: KnownImports = {
object: () => E.right({ type: 'object', properties: {}, required: [] }),
type: (_, schema) => {
if (schema.type !== 'object') {
return E.left('typeC parameter must be object');
return errorLeft('typeC parameter must be object');
}
const props = Object.entries(schema.properties).reduce((acc, [key, prop]) => {
return { ...acc, [key]: prop };
Expand All @@ -73,7 +74,7 @@ export const KNOWN_IMPORTS: KnownImports = {
},
partial: (_, schema) => {
if (schema.type !== 'object') {
return E.left('typeC parameter must be object');
return errorLeft('typeC parameter must be object');
}
const props = Object.entries(schema.properties).reduce((acc, [key, prop]) => {
return { ...acc, [key]: prop };
Expand All @@ -83,7 +84,7 @@ export const KNOWN_IMPORTS: KnownImports = {
exact: (_, schema) => E.right(schema),
strict: (_, schema) => {
if (schema.type !== 'object') {
return E.left('exactC parameter must be object');
return errorLeft('exactC parameter must be object');
}
const props = Object.entries(schema.properties).reduce((acc, [key, prop]) => {
return { ...acc, [key]: prop };
Expand All @@ -96,33 +97,33 @@ export const KNOWN_IMPORTS: KnownImports = {
},
record: (_, domain, codomain) => {
if (!codomain) {
return E.left('Codomain of record must be specified');
return errorLeft('Codomain of record must be specified');
} else {
return E.right({ type: 'record', domain, codomain });
}
},
union: (_, schema) => {
if (schema.type !== 'tuple') {
return E.left('unionC parameter must be array');
return errorLeft('unionC parameter must be array');
}
return E.right({ type: 'union', schemas: schema.schemas });
},
intersection: (_, schema) => {
if (schema.type !== 'tuple') {
return E.left('unionC parameter must be array');
return errorLeft('unionC parameter must be array');
}
return E.right({ type: 'intersection', schemas: schema.schemas });
},
literal: (_, arg) => {
if (!isPrimitive(arg) || arg.enum === undefined) {
return E.left(`Unimplemented literal type ${arg.type}`);
return errorLeft(`Unimplemented literal type ${arg.type}`);
} else {
return E.right(arg);
}
},
keyof: (_, arg) => {
if (arg.type !== 'object') {
return E.left(`Unimplemented keyof type ${arg.type}`);
return errorLeft(`Unimplemented keyof type ${arg.type}`);
}
const schemas: Schema[] = Object.keys(arg.properties).map((prop) => ({
type: 'string',
Expand Down Expand Up @@ -377,7 +378,7 @@ export const KNOWN_IMPORTS: KnownImports = {
E.right({ type: 'union', schemas: [innerSchema, { type: 'undefined' }] }),
optionalized: (_, props) => {
if (props.type !== 'object') {
return E.left('optionalized parameter must be object');
return errorLeft('optionalized parameter must be object');
}
const required = Object.keys(props.properties).filter(
(key) => !isOptional(props.properties[key]!),
Expand All @@ -386,7 +387,7 @@ export const KNOWN_IMPORTS: KnownImports = {
},
httpRequest: (deref, arg) => {
if (arg.type !== 'object') {
return E.left(`Unimplemented httpRequest type ${arg.type}`);
return errorLeft(`Unimplemented httpRequest type ${arg.type}`);
}
const properties: Record<string, Schema> = {};
for (const [outerKey, outerValue] of Object.entries(arg.properties)) {
Expand All @@ -397,7 +398,7 @@ export const KNOWN_IMPORTS: KnownImports = {
}
const innerProps = innerPropsE.right;
if (innerProps.type !== 'object') {
return E.left(`Unimplemented httpRequest type ${innerProps.type}`);
return errorLeft(`Unimplemented httpRequest type ${innerProps.type}`);
}

innerProps.required = innerProps.required.filter(
Expand All @@ -416,7 +417,7 @@ export const KNOWN_IMPORTS: KnownImports = {
},
httpRoute: (deref, schema) => {
if (schema.type !== 'object') {
return E.left('httpRoute parameter must be object');
return errorLeft('httpRoute parameter must be object');
}
const props: Record<string, Schema> = {};
for (const [key, value] of Object.entries(schema.properties)) {
Expand Down
9 changes: 5 additions & 4 deletions packages/openapi-generator/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import resolve from 'resolve';

import { KNOWN_IMPORTS, type KnownCodec } from './knownImports';
import { parseSource, type SourceFile } from './sourceFile';
import { errorLeft } from './error';

const readFile = promisify(fs.readFile);

Expand Down Expand Up @@ -110,7 +111,7 @@ export class Project {
}

if (!typesEntryPoint) {
return E.left(`Could not find types entry point for ${library}`);
return errorLeft(`Could not find types entry point for ${library}`);
}

const entryPoint = resolve.sync(`${library}/${typesEntryPoint}`, {
Expand All @@ -119,7 +120,7 @@ export class Project {
});
return E.right(entryPoint);
} catch (err) {
return E.left(`Could not resolve entry point for ${library}: ${err}`);
return errorLeft(`Could not resolve entry point for ${library}: ${err}`);
}
}

Expand All @@ -132,10 +133,10 @@ export class Project {
return E.right(result);
} catch (e: unknown) {
if (e instanceof Error && e.message) {
return E.left(e.message);
return errorLeft(e.message);
}

return E.left(JSON.stringify(e));
return errorLeft(JSON.stringify(e));
}
}

Expand Down
Loading

0 comments on commit b7c83a6

Please sign in to comment.