diff --git a/packages/core/src/code-gen/__snapshots__/parser.interface.test.ts.snap b/packages/core/src/code-gen/__snapshots__/parser.interface.test.ts.snap index 912cc19d..e6f2e6ae 100644 --- a/packages/core/src/code-gen/__snapshots__/parser.interface.test.ts.snap +++ b/packages/core/src/code-gen/__snapshots__/parser.interface.test.ts.snap @@ -1,5 +1,64 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Parsers extend an intersection of two interfaces 1`] = ` +"export class MyModelParser { + static parse(data: any): ParseResult { + const errors: ValidationError[] = []; + let result: any; + { + const intersectionResult: any = {}; + { + const parseResult = CParser.parse(data); + if (!parseResult.ok) { + errors.push({ + path: \\"data\\", + message: \`not a MyModel\` + }); + } + else + result = parseResult.result; + } + Object.assign(intersectionResult, result); + if (typeof data !== \\"object\\" || data === null) { + errors.push({ + path: \\"data\\", + message: \`null or not an object\` + }); + } + else { + result = {}; + if (typeof data[\\"c\\"] !== \\"number\\") { + errors.push({ + path: \\"data.c\\", + message: \`not a number\` + }); + } + else if (isNaN(data[\\"c\\"])) { + errors.push({ + path: \\"data.c\\", + message: \`invalid number\` + }); + } + else + result[\\"c\\"] = data[\\"c\\"]; + } + Object.assign(intersectionResult, result); + result = intersectionResult; + } + if (errors.length) { + return { + ok: false, + errors + }; + } + return { + ok: true, + result: result as MyModel + }; + } +}" +`; + exports[`Parsers for an interface boolean array member 1`] = ` "export class MyModelParser { static parse(data: any): ParseResult { diff --git a/packages/core/src/code-gen/__snapshots__/parser.typealias.test.ts.snap b/packages/core/src/code-gen/__snapshots__/parser.typealias.test.ts.snap index 3a27d7f0..6fd39f64 100644 --- a/packages/core/src/code-gen/__snapshots__/parser.typealias.test.ts.snap +++ b/packages/core/src/code-gen/__snapshots__/parser.typealias.test.ts.snap @@ -855,12 +855,120 @@ exports[`Parsers for a type alias index types union string keys 1`] = ` }" `; +exports[`Parsers for a type alias intersection intersection with two interfaces 1`] = ` +"export class MyModelParser { + static parse(data: any): ParseResult { + const errors: ValidationError[] = []; + let result: any; + { + const intersectionResult: any = {}; + { + const parseResult = AParser.parse(data); + if (!parseResult.ok) { + errors.push({ + path: \\"data\\", + message: \`not a A\` + }); + } + else + result = parseResult.result; + } + Object.assign(intersectionResult, result); + { + const parseResult = BParser.parse(data); + if (!parseResult.ok) { + errors.push({ + path: \\"data\\", + message: \`not a B\` + }); + } + else + result = parseResult.result; + } + Object.assign(intersectionResult, result); + result = intersectionResult; + } + if (errors.length) { + return { + ok: false, + errors + }; + } + return { + ok: true, + result: result as MyModel + }; + } +}" +`; + +exports[`Parsers for a type alias intersection intersection with two types 1`] = ` +"export class MyModelParser { + static parse(data: any): ParseResult { + const errors: ValidationError[] = []; + let result: any; + { + const errLength = errors.length; + { + { + const parseResult = AParser.parse(data); + if (!parseResult.ok) { + errors.push({ + path: \\"data\\", + message: \`not a A\` + }); + } + else + result = parseResult.result; + } + if (errors.length !== errLength) { + errors.splice(errLength, errors.length - errLength); + { + { + const parseResult = BParser.parse(data); + if (!parseResult.ok) { + errors.push({ + path: \\"data\\", + message: \`not a B\` + }); + } + else + result = parseResult.result; + } + if (errors.length !== errLength) { + errors.splice(errLength, errors.length - errLength); + { + errors.push({ + path: \\"data\\", + message: \`is none of the options of union\` + }); + } + } + } + } + } + } + if (errors.length) { + return { + ok: false, + errors + }; + } + return { + ok: true, + result: result as MyModel + }; + } +}" +`; + exports[`Parsers for a type alias intersection object literal with complex union 1`] = ` "export class MyModelParser { static parse(data: any): ParseResult { const errors: ValidationError[] = []; let result: any; { + const intersectionResult: any = {}; if (typeof data !== \\"object\\" || data === null) { errors.push({ path: \\"data\\", @@ -878,6 +986,7 @@ exports[`Parsers for a type alias intersection object literal with complex union else result[\\"a\\"] = data[\\"a\\"]; } + Object.assign(intersectionResult, result); { const errLength = errors.length; { @@ -962,6 +1071,8 @@ exports[`Parsers for a type alias intersection object literal with complex union } } } + Object.assign(intersectionResult, result); + result = intersectionResult; } if (errors.length) { return { @@ -983,6 +1094,7 @@ exports[`Parsers for a type alias intersection object literal with union 1`] = ` const errors: ValidationError[] = []; let result: any; { + const intersectionResult: any = {}; if (typeof data !== \\"object\\" || data === null) { errors.push({ path: \\"data\\", @@ -1000,6 +1112,7 @@ exports[`Parsers for a type alias intersection object literal with union 1`] = ` else result[\\"a\\"] = data[\\"a\\"]; } + Object.assign(intersectionResult, result); { const errLength = errors.length; { @@ -1059,6 +1172,8 @@ exports[`Parsers for a type alias intersection object literal with union 1`] = ` } } } + Object.assign(intersectionResult, result); + result = intersectionResult; } if (errors.length) { return { @@ -1080,6 +1195,7 @@ exports[`Parsers for a type alias intersection simple 1`] = ` const errors: ValidationError[] = []; let result: any; { + const intersectionResult: any = {}; if (typeof data !== \\"object\\" || data === null) { errors.push({ path: \\"data\\", @@ -1097,6 +1213,7 @@ exports[`Parsers for a type alias intersection simple 1`] = ` else result[\\"a\\"] = data[\\"a\\"]; } + Object.assign(intersectionResult, result); if (typeof data !== \\"object\\" || data === null) { errors.push({ path: \\"data\\", @@ -1114,6 +1231,8 @@ exports[`Parsers for a type alias intersection simple 1`] = ` else result[\\"b\\"] = data[\\"b\\"]; } + Object.assign(intersectionResult, result); + result = intersectionResult; } if (errors.length) { return { diff --git a/packages/core/src/code-gen/parser.interface.test.ts b/packages/core/src/code-gen/parser.interface.test.ts index 3516ba04..89acead4 100644 --- a/packages/core/src/code-gen/parser.interface.test.ts +++ b/packages/core/src/code-gen/parser.interface.test.ts @@ -1,5 +1,5 @@ import ts from "typescript" -import { compileStatement, printCode } from "../tsTestUtils" +import { compileStatement, compileStatements, printCode } from "../tsTestUtils" import generateParser from "./parsers/generateParser" describe("Parsers", () => { @@ -263,4 +263,31 @@ describe("Parsers", () => { expect(printCode(parserDeclaration)).toMatchSnapshot() }) }) + test("extend an intersection of two interfaces", () => { + const { + statements: [model], + typeChecker, + } = compileStatements( + ` + interface MyModel extends C { + c: number + } + + type C = A & B + + interface A { + a: number + } + interface B { + b: number + } + `, + ) + + const parserDeclaration = generateParser( + model as ts.TypeAliasDeclaration, + typeChecker, + ) + expect(printCode(parserDeclaration)).toMatchSnapshot() + }) }) diff --git a/packages/core/src/code-gen/parser.typealias.test.ts b/packages/core/src/code-gen/parser.typealias.test.ts index f79a5e28..fea1c85d 100644 --- a/packages/core/src/code-gen/parser.typealias.test.ts +++ b/packages/core/src/code-gen/parser.typealias.test.ts @@ -357,6 +357,53 @@ describe("Parsers", () => { const parserDeclaration = generateParser(model, typeChecker) expect(printCode(parserDeclaration)).toMatchSnapshot() }) + + test("intersection with two types", () => { + const { + statements: [model], + typeChecker, + } = compileStatements( + ` + type MyModel = A | B + + type A = { + a: number + } + type B = { + b: number + } + `, + ) + + const parserDeclaration = generateParser( + model as ts.TypeAliasDeclaration, + typeChecker, + ) + expect(printCode(parserDeclaration)).toMatchSnapshot() + }) + test("intersection with two interfaces", () => { + const { + statements: [model], + typeChecker, + } = compileStatements( + ` + type MyModel = A & B + + interface A { + a: number + } + interface B { + b: number + } + `, + ) + + const parserDeclaration = generateParser( + model as ts.TypeAliasDeclaration, + typeChecker, + ) + expect(printCode(parserDeclaration)).toMatchSnapshot() + }) }) describe("typescript utility types", () => { test("Pick", () => { diff --git a/packages/core/src/code-gen/parsers/generateIntersectionParser.ts b/packages/core/src/code-gen/parsers/generateIntersectionParser.ts index 47d029a1..335c3661 100644 --- a/packages/core/src/code-gen/parsers/generateIntersectionParser.ts +++ b/packages/core/src/code-gen/parsers/generateIntersectionParser.ts @@ -1,4 +1,5 @@ import ts from "typescript" +import * as tsx from "../../tsx" import generateParserFromModel from "./generateParserFromModel" import { IntersectionParserModel } from "./generateParserModel" import Pointer from "./Pointer" @@ -6,9 +7,26 @@ import Pointer from "./Pointer" export default function generateIntersectionParser( pointer: Pointer, ): ts.Statement { - return ts.factory.createBlock( - pointer.model.parsers.map((parser) => + return ts.factory.createBlock([ + tsx.const({ + name: "intersectionResult", + init: tsx.literal.object(), + type: tsx.type.any, + }), + ...pointer.model.parsers.flatMap((parser) => [ generateParserFromModel(parser, pointer.path), + tsx.statement.expression( + tsx.expression.call(tsx.expression.propertyAccess("Object", "assign"), { + args: ["intersectionResult", "result"], + }), + ), + ]), + tsx.statement.expression( + tsx.expression.binary( + tsx.expression.identifier("result"), + "=", + tsx.expression.identifier("intersectionResult"), + ), ), - ) + ]) } diff --git a/packages/core/src/code-gen/parsers/generateParserModel.ts b/packages/core/src/code-gen/parsers/generateParserModel.ts index f79514d5..8b43d58a 100644 --- a/packages/core/src/code-gen/parsers/generateParserModel.ts +++ b/packages/core/src/code-gen/parsers/generateParserModel.ts @@ -186,6 +186,27 @@ export default function generateParserModel( const typeName = typeChecker.typeToString(type, rootNode, undefined) if (ts.isInterfaceDeclaration(rootNode)) { + const heritageReferenceParsers = (rootNode.heritageClauses ?? []) + .flatMap((heritageClause) => heritageClause.types) + .map((heritageType) => generateReferenceParser(heritageType, 0)) + + const objectParser: ObjectParserModel = { + type: ParserModelType.Object, + members: rootNode.members.reduce((members, member) => { + return member.name + ? [ + ...members, + { + type: ParserModelType.Member, + name: getMemberName(member.name), + optional: !!member.questionToken, + parser: generate(member, 0), + }, + ] + : members + }, [] as MemberParserModel[]), + } + return { type: ParserModelType.Root, name: rootName, @@ -205,22 +226,12 @@ export default function generateParserModel( }, })) ?? [], }, - parser: { - type: ParserModelType.Object, - members: rootNode.members.reduce((members, member) => { - return member.name - ? [ - ...members, - { - type: ParserModelType.Member, - name: getMemberName(member.name), - optional: !!member.questionToken, - parser: generate(member, 0), - }, - ] - : members - }, [] as MemberParserModel[]), - }, + parser: heritageReferenceParsers.length + ? { + type: ParserModelType.Intersection, + parsers: [...heritageReferenceParsers, objectParser], + } + : objectParser, } } else if (ts.isTypeAliasDeclaration(rootNode)) { return { @@ -504,22 +515,7 @@ export default function generateParserModel( } else if ((type.flags & ts.TypeFlags.Object) === ts.TypeFlags.Object) { return generateObjectType(type, node, depth) } else if (type.isUnionOrIntersection()) { - return { - type: ParserModelType.Reference, - baseTypeName: getMemberName(node.typeName), - typeName: typeChecker.typeToString(type, node, undefined), - fullyQualifiedName: getFullyQualifiedName(node, typeChecker), - typeArguments: - node.typeArguments?.map((typeArg) => ({ - typeName: typeChecker.typeToString( - typeChecker.getTypeFromTypeNode(typeArg), - ), - fullyQualifiedName: ts.isTypeReferenceNode(typeArg) - ? getFullyQualifiedName(typeArg, typeChecker) - : undefined, - parser: generate(typeArg, depth), - })) ?? [], - } + return generateReferenceParser(node, depth) } } @@ -617,22 +613,7 @@ export default function generateParserModel( }, []), } } - return { - type: ParserModelType.Reference, - baseTypeName: getMemberName(node.typeName), - typeName: typeChecker.typeToString(type, node, undefined), - fullyQualifiedName: getFullyQualifiedName(node, typeChecker), - typeArguments: - node.typeArguments?.map((typeArg) => ({ - typeName: typeChecker.typeToString( - typeChecker.getTypeFromTypeNode(typeArg), - ), - fullyQualifiedName: ts.isTypeReferenceNode(typeArg) - ? getFullyQualifiedName(typeArg, typeChecker) - : undefined, - parser: generate(typeArg, depth), - })) ?? [], - } + return generateReferenceParser(node, depth) } function getMemberName( @@ -703,4 +684,36 @@ export default function generateParserModel( }), } } + + function generateReferenceParser( + node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, + depth: number, + ): ReferenceParserModel { + const baseTypeName = ts.isTypeReferenceNode(node) + ? getMemberName(node.typeName) + : ts.isIdentifier(node.expression) + ? node.expression.text + : undefined + + if (!baseTypeName) { + throw new ParseError("Unexpected node", node) + } + + return { + type: ParserModelType.Reference, + baseTypeName, + typeName: typeChecker.typeToString(type, node, undefined), + fullyQualifiedName: getFullyQualifiedName(node, typeChecker), + typeArguments: + node.typeArguments?.map((typeArg) => ({ + typeName: typeChecker.typeToString( + typeChecker.getTypeFromTypeNode(typeArg), + ), + fullyQualifiedName: ts.isTypeReferenceNode(typeArg) + ? getFullyQualifiedName(typeArg, typeChecker) + : undefined, + parser: generate(typeArg, depth), + })) ?? [], + } + } } diff --git a/packages/core/src/tsUtils.ts b/packages/core/src/tsUtils.ts index 35f11055..091dfff4 100644 --- a/packages/core/src/tsUtils.ts +++ b/packages/core/src/tsUtils.ts @@ -307,7 +307,7 @@ export function getNameAsString( } export function getFullyQualifiedName( - typeNode: ts.TypeReferenceNode, + typeNode: ts.NodeWithTypeArguments, typeChecker: ts.TypeChecker, ): { base: string diff --git a/packages/core/src/tsx/binaryExpression.ts b/packages/core/src/tsx/binaryExpression.ts index 1b234009..ba9ee12d 100644 --- a/packages/core/src/tsx/binaryExpression.ts +++ b/packages/core/src/tsx/binaryExpression.ts @@ -1,6 +1,7 @@ import ts from "typescript" type BinaryOperator = + | "=" | "==" | "===" | "<=" @@ -28,6 +29,8 @@ function generateOperator( op: BinaryOperator, ): ts.BinaryOperator | ts.BinaryOperatorToken { switch (op) { + case "=": + return ts.factory.createToken(ts.SyntaxKind.EqualsToken) case "==": return ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken) case "===":