diff --git a/package.json b/package.json index 3a2bc9c..2a22522 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "prepublish": "yarn run build" }, "peerDependencies": { - "@hexancore/common": "^0.16.0", + "@hexancore/common": "^0.16.1", "@nestjs/common": "^10.3.9", "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.3.9", @@ -103,7 +103,7 @@ "tslib": "^2.6.3" }, "devDependencies": { - "@hexancore/common": "^0.16.0", + "@hexancore/common": "0.16.1", "@hexancore/mocker": "^1.1.2", "@nestjs/cli": "^10.3.2", "@nestjs/common": "^10.3.9", @@ -131,7 +131,7 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "5.4.5" + "typescript": "5.6.2" }, "bugs": { "url": "https://github.com/hexancore/core/issues" diff --git a/src/Compiler/Test/TsTransformerTestHelper.ts b/src/Compiler/Test/TsTransformerTestHelper.ts index df1c5cc..d9b8aee 100644 --- a/src/Compiler/Test/TsTransformerTestHelper.ts +++ b/src/Compiler/Test/TsTransformerTestHelper.ts @@ -43,11 +43,10 @@ export class TsTransformerTestHelper { ); } - public createTransformerFactory(transformer: (sourceFile: ts.SourceFile, context: ts.TransformationContext,) => ts.SourceFile): ts.TransformerFactory { + public createTransformerFactory(transformer: ContextAwareCustomTransformer): ts.TransformerFactory { return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => transformer(sourceFile, context); } - public transformExistingAndReturnAsString(sourceFilePath: string, transformers: ts.TransformerFactory[]): string { const sourceFile = this.createSourceFileFromExisting(sourceFilePath); return this.transformAndReturnAsString(sourceFile, transformers); @@ -57,6 +56,7 @@ export class TsTransformerTestHelper { const transformResult = ts.transform(sourceFile, transformers, this.compilerOptions); const transformedSourceFile = transformResult.transformed[0]; + const printer = ts.createPrinter(); const result = printer.printNode( diff --git a/src/Compiler/Transformer/Feature/FeatureTsTransformer.ts b/src/Compiler/Transformer/Feature/FeatureTsTransformer.ts index a84999d..6d895d1 100644 --- a/src/Compiler/Transformer/Feature/FeatureTsTransformer.ts +++ b/src/Compiler/Transformer/Feature/FeatureTsTransformer.ts @@ -72,6 +72,7 @@ export class FeatureTsTransformer { for (const t of this.transformers) { if (t.supports(featureSourceTransformContext.featureSourcePath, featureSourceTransformContext.feature)) { const transformed = t.transform(source, featureSourceTransformContext); + TsTransfromerHelper.reportDiagnostics(featureSourceTransformContext.diagnostics); return transformed; } } diff --git a/src/Compiler/Transformer/Feature/HObject/HObjectConstructorTsFactory.ts b/src/Compiler/Transformer/Feature/HObject/HObjectConstructorTsFactory.ts index 012cbbb..ff23572 100644 --- a/src/Compiler/Transformer/Feature/HObject/HObjectConstructorTsFactory.ts +++ b/src/Compiler/Transformer/Feature/HObject/HObjectConstructorTsFactory.ts @@ -3,6 +3,9 @@ import type { HObjectPropertyTsMeta } from "./HObjectPropertyTsMeta"; export class HObjectToConstructorTsFactory { public create(properties: HObjectPropertyTsMeta[]): ts.ConstructorDeclaration { + + const callSuper = ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createSuper(), undefined, undefined)); + const propertiesSorted = [...properties.filter(p => !p.optional), ...properties.filter(p => p.optional)]; const constructorParameters = propertiesSorted.map(p => this.createParameter(p)); const constructorAssignments = propertiesSorted.map(p => this.createAssignment(p)); @@ -10,7 +13,7 @@ export class HObjectToConstructorTsFactory { return ts.factory.createConstructorDeclaration( [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], constructorParameters, - ts.factory.createBlock(constructorAssignments, true) + ts.factory.createBlock([callSuper, ...constructorAssignments], true) ); } diff --git a/src/Compiler/Transformer/Feature/HObject/HObjectParseTsFactory.ts b/src/Compiler/Transformer/Feature/HObject/HObjectParseTsFactory.ts index 317a1a7..17c762c 100644 --- a/src/Compiler/Transformer/Feature/HObject/HObjectParseTsFactory.ts +++ b/src/Compiler/Transformer/Feature/HObject/HObjectParseTsFactory.ts @@ -1,52 +1,105 @@ -import ts from "typescript"; +import ts, { type Identifier } from "typescript"; import type { HObjectPropertyTsMeta } from "./HObjectPropertyTsMeta"; import type { ImportDeclarationWrapper } from "../../Helper/ImportDeclarationWrapper"; import { TsTransfromerHelper } from "../../TsTransformerHelper"; import { HObjectPropertyParseTsFactory } from "./HObjectPropertyParseTsFactory"; +import { HObjectKind, type FeatureHObjectMeta } from "@/Util/Feature/Meta"; + +const HOBJECT_ANY_TYPE_MAP = { + [HObjectKind.Command]: { + anyTypeName: "AnyHCommand", + typeName: "HCommandType" + }, + [HObjectKind.Query]: { + anyTypeName: "AnyHQuery", + typeName: "HQueryType" + }, + [HObjectKind.Event]: { + anyTypeName: "AnyHEvent", + typeName: "HEventType" + }, + [HObjectKind.Dto]: { + anyTypeName: "AnyDto", + typeName: "HDtoType" + }, + [HObjectKind.ValueObject]: { + anyTypeName: "AnyValueObject", + typeName: "HValueObjectType" + }, + [HObjectKind.Entity]: { + anyTypeName: "AnyEntity", + typeName: "HEntityType" + }, + [HObjectKind.AggregateRoot]: { + anyTypeName: "AnyAggregateRoot", + typeName: "HAggregateRootType" + } +}; export class HObjectParseTsFactory { - private propertyTsFactory: HObjectPropertyParseTsFactory; public constructor() { this.propertyTsFactory = new HObjectPropertyParseTsFactory(); } - public create(hObjectClassName: string, properties: HObjectPropertyTsMeta[], hCommonImportDecl: ImportDeclarationWrapper): ts.MethodDeclaration { + //`public static parse(this: DtoType, plain: unknown): R` + public create(meta: FeatureHObjectMeta, hObjectClassName: string, properties: HObjectPropertyTsMeta[], hCommonImportDecl: ImportDeclarationWrapper): ts.MethodDeclaration { const returnType = ts.factory.createTypeReferenceNode( hCommonImportDecl.getEntityName('R'), [ts.factory.createTypeReferenceNode(hObjectClassName, [])] ); + const typeInfo = HOBJECT_ANY_TYPE_MAP[meta.kind]; + const parameters = [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier('this'), + undefined, + ts.factory.createTypeReferenceNode(typeInfo.typeName, [ts.factory.createTypeReferenceNode("T")]) + ), ts.factory.createParameterDeclaration(undefined, undefined, 'plain', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)) ]; + const genericT = ts.factory.createTypeParameterDeclaration( + undefined, + ts.factory.createIdentifier("T"), + ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(typeInfo.anyTypeName), undefined) + ); + return ts.factory.createMethodDeclaration( [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword), ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)], undefined, // * 'parse', undefined, // '?' - undefined, // generic + [genericT], parameters, returnType, - ts.factory.createBlock(this.createBody(hObjectClassName, properties), true) + ts.factory.createBlock(this.createBody(hObjectClassName, properties, hCommonImportDecl), true) ); } - private createBody(hObjectClassName: string, properties: HObjectPropertyTsMeta[]): ts.Statement[] { + private createBody(hObjectClassName: string, properties: HObjectPropertyTsMeta[], hCommonImportDecl: ImportDeclarationWrapper): ts.Statement[] { + properties = [...properties.filter(p => !p.optional), ...properties.filter(p => p.optional)]; const statements: ts.Statement[] = [ - this.createCheckIsObject(hObjectClassName), + this.createCheckIsObject(hObjectClassName, hCommonImportDecl), this.createPlainObjVarDeclaration(hObjectClassName), this.createIssuesVarDeclaration(), ]; + const propNames: string[] = []; for (const p of properties) { - statements.push(...this.propertyTsFactory.create(p)); + statements.push(...this.propertyTsFactory.create(p, hCommonImportDecl)); + propNames.push(p.name); } + statements.push(this.createCheckAnyIssues(hObjectClassName, hCommonImportDecl)); + statements.push(this.createReturnNew(hObjectClassName, hCommonImportDecl, propNames)); + return statements; } @@ -57,7 +110,7 @@ export class HObjectParseTsFactory { } ` */ - private createCheckIsObject(hObjectClassName: string): ts.Statement { + private createCheckIsObject(hObjectClassName: string, hCommonImportDecl: ImportDeclarationWrapper): ts.Statement { // `typeof plain !== 'object'` const condtionExpression = ts.factory.createBinaryExpression( ts.factory.createTypeOfExpression(ts.factory.createIdentifier('plain')), @@ -69,15 +122,12 @@ export class HObjectParseTsFactory { ts.factory.createReturnStatement( ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('PlainParseHelper'), + hCommonImportDecl.get('PlainParseHelper'), 'HObjectIsNotObjectParseErr' ), undefined, [ - ts.factory.createAsExpression( - ts.factory.createIdentifier(hObjectClassName), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - ), + this.createAsAny(hObjectClassName), ts.factory.createIdentifier('plain') ] ) @@ -109,4 +159,67 @@ export class HObjectParseTsFactory { return TsTransfromerHelper.createConstStatement("issues", type, initializer); } + + private createCheckAnyIssues(hObjectClassName: string, hCommonImportDecl: ImportDeclarationWrapper): ts.Statement { + return ts.factory.createIfStatement( + ts.factory.createBinaryExpression( + ts.factory.createPropertyAccessExpression( + this.issuesVarIdentifier(), + ts.factory.createIdentifier('length') + ), + ts.factory.createToken(ts.SyntaxKind.GreaterThanToken), + ts.factory.createNumericLiteral('0') + ), + ts.factory.createBlock( + [ + ts.factory.createReturnStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + hCommonImportDecl.get('PlainParseHelper'), + ts.factory.createIdentifier('HObjectParseErr') + ), + undefined, + [ + this.createAsAny(hObjectClassName), + this.issuesVarIdentifier(), + ] + ) + ) + ], + true + ), + undefined + ); + } + + private createReturnNew(hObjectClassName: string, hCommonImportDecl: ImportDeclarationWrapper, propNames: string[]) { + return ts.factory.createReturnStatement( + ts.factory.createAsExpression( + ts.factory.createCallExpression( + hCommonImportDecl.get("OK"), + undefined, + [ + ts.factory.createNewExpression( + hCommonImportDecl.get(hObjectClassName), + undefined, + propNames.map((name) => this.createAsAny(name)) + ) + ] + ), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) + ) + ); + } + + private issuesVarIdentifier(): ts.Identifier { + return ts.factory.createIdentifier("issues"); + } + + private createAsAny(name: string): ts.Expression { + return ts.factory.createAsExpression( + ts.factory.createIdentifier(name), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) + ); + } + } \ No newline at end of file diff --git a/src/Compiler/Transformer/Feature/HObject/HObjectPropertyExtractor.ts b/src/Compiler/Transformer/Feature/HObject/HObjectPropertyExtractor.ts index d7f4606..b36b7b1 100644 --- a/src/Compiler/Transformer/Feature/HObject/HObjectPropertyExtractor.ts +++ b/src/Compiler/Transformer/Feature/HObject/HObjectPropertyExtractor.ts @@ -1,20 +1,36 @@ -import ts from "typescript"; +import ts, { SyntaxKind } from "typescript"; +import { LogicError } from "@hexancore/common"; import { TsTransfromerHelper } from "../../TsTransformerHelper"; -import { CollectionType, HObjectPropertyKind, HObjectPropertyTsMeta, HObjectPropertyPrimitiveType, ValueValidationRule } from "./HObjectPropertyTsMeta"; +import { CollectionType, HObjectPropertyKind, HObjectPropertyTsMeta, HObjectPropertyPrimitiveType, ValueValidationRule, type HObjectPropertyType, PrimitiveHObjectPropertyTsMeta, ObjectHObjectPropertyTsMeta } from "./HObjectPropertyTsMeta"; +import type { ImportDeclarationWrapper } from "../../Helper/ImportDeclarationWrapper"; + +interface HObjectExtractPropertyTypeMeta { + kind: HObjectPropertyKind; + tsType: HObjectPropertyType; + validationRules?: ValueValidationRule[], + collectionType?: CollectionType, + tsImportDeclaration?: ImportDeclarationWrapper; +} + +export interface HObjectPropertyExtractContext { + sourceFile: ts.SourceFile; + diagnostics: ts.Diagnostic[]; + importNameToDeclarationMap: Map; +} export class HObjectPropertyExtractor { - public extract(node: ts.ClassDeclaration, sourceFile: ts.SourceFile, diagnostics: ts.Diagnostic[]): HObjectPropertyTsMeta[] { + public extract(node: ts.ClassDeclaration, context: HObjectPropertyExtractContext): HObjectPropertyTsMeta[] { const properties: HObjectPropertyTsMeta[] = []; node.members.forEach(member => { if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) { - const meta = this.extractPropertyMeta(member, sourceFile, diagnostics); + const meta = this.extractPropertyMeta(member, context); if (meta) { properties.push(meta); } } if (ts.isConstructorDeclaration(member)) { - diagnostics.push(TsTransfromerHelper.createDiagnostic(node, "HObject user defined constructor is unsupported")); + context.diagnostics.push(TsTransfromerHelper.createDiagnostic(member, "HObject user defined constructor is unsupported", context.sourceFile)); //this.collectFromConstructor(member, properties, sourceFile, diagnostics); } }, this); @@ -22,6 +38,7 @@ export class HObjectPropertyExtractor { return properties; } + /* private collectFromConstructor(node: ts.ConstructorDeclaration, properties: HObjectPropertyTsMeta[], sourceFile: ts.SourceFile, diagnostics: ts.Diagnostic[]): void { for (const param of node.parameters) { if (ts.isParameterPropertyDeclaration(param, node) && ts.isIdentifier(param.name)) { @@ -32,40 +49,52 @@ export class HObjectPropertyExtractor { } } } + */ - private extractPropertyMeta(member: ts.PropertyDeclaration | ts.ParameterPropertyDeclaration, sourceFile: ts.SourceFile, diagnostics: ts.Diagnostic[]) { + private extractPropertyMeta(member: ts.PropertyDeclaration | ts.ParameterPropertyDeclaration, context: HObjectPropertyExtractContext) { if (member.type === undefined) { - diagnostics.push(TsTransfromerHelper.createDiagnostic(member, 'Property has type "any", which is not allowed in HObject')); + context.diagnostics.push(TsTransfromerHelper.createDiagnostic(member, 'Property has type "any", which is not allowed in HObject', context.sourceFile)); return undefined; } - const typeMeta = this.extractPropertyType(member, member.type, sourceFile, diagnostics); + const typeMeta = this.extractPropertyType(member, member.type, context); if (!typeMeta) { return; } - return new HObjectPropertyTsMeta( - typeMeta.kind, - (member.name as ts.Identifier).text, - typeMeta.tsType, - !!member.questionToken, - typeMeta.validationRules, - typeMeta.collectionType - ); + const name = (member.name as ts.Identifier).text; + const optional = !!member.questionToken; + if (typeMeta.kind === HObjectPropertyKind.PRIMITIVE) { + return new PrimitiveHObjectPropertyTsMeta( + name, + typeMeta.tsType as any, + optional, + typeMeta.validationRules, + typeMeta.collectionType + ); + } else { + return new ObjectHObjectPropertyTsMeta( + name, + typeMeta.tsType as any, + typeMeta.tsImportDeclaration!, + optional, + typeMeta.validationRules, + typeMeta.collectionType + ); + } } private extractPropertyType( parentNode: ts.Node, typeNode: ts.TypeNode, - sourceFile: ts.SourceFile, - diagnostics: ts.Diagnostic[] - ): Pick | undefined { + context: HObjectPropertyExtractContext, + ): HObjectExtractPropertyTypeMeta | undefined { // v.int.between<10, 100>[] & v.items.exactly<10> // v.int[] & v.items.exactly<10> // string[] & v.items.exactly<10> if (ts.isIntersectionTypeNode(typeNode)) { - return this.extractTypeFromIntesectionTypeNode(parentNode, typeNode, sourceFile, diagnostics); + return this.extractTypeFromIntesectionTypeNode(parentNode, typeNode, context); } // TODO: support type union @@ -74,64 +103,61 @@ export class HObjectPropertyExtractor { // v.int[] // string[] if (ts.isArrayTypeNode(typeNode)) { - return this.extractTypeFromArrayTypeNode(parentNode, typeNode, sourceFile, diagnostics); + return this.extractTypeFromArrayTypeNode(parentNode, typeNode, context); } // v.int.between<10, 100> // v.int // string - return this.extractTypeFromNode(parentNode, typeNode, sourceFile, diagnostics); + return this.extractTypeFromNode(parentNode, typeNode, context); } private extractTypeFromIntesectionTypeNode( parentNode: ts.Node, typeNode: ts.IntersectionTypeNode, - sourceFile: ts.SourceFile, - diagnostics: ts.Diagnostic[] - ): Pick | undefined { + context: HObjectPropertyExtractContext + ): HObjectExtractPropertyTypeMeta | undefined { if (ts.isArrayTypeNode(typeNode.types[0])) { - const itemType = this.extractTypeFromNode(parentNode, typeNode.types[0].elementType, sourceFile, diagnostics); + const itemType = this.extractTypeFromNode(parentNode, typeNode.types[0].elementType, context); if (!itemType) { return undefined; } - const itemsRuleType = this.extractTypeFromNode(parentNode, typeNode.types[1], sourceFile, diagnostics); + const itemsRuleType = this.extractTypeFromNode(parentNode, typeNode.types[1], context); if (!itemsRuleType) { return itemType; } - if (itemsRuleType.validationRules && itemsRuleType.validationRules[0].rule.startsWith('v.items')) { + if (itemsRuleType.validationRules && itemsRuleType.validationRules[0].isCollectionItemsRule()) { return { tsType: itemType.tsType, kind: itemType.kind, validationRules: [...itemType.validationRules!, itemsRuleType.validationRules[0]], collectionType: CollectionType.Array, + tsImportDeclaration: itemType.tsImportDeclaration, }; } else { - diagnostics.push(TsTransfromerHelper.createDiagnostic(parentNode, 'Only v.items.* can be used for array HObject property')); + context.diagnostics.push(TsTransfromerHelper.createDiagnostic(parentNode, 'v.items.* can be used only for `Array|Set|Map` property', context.sourceFile)); return undefined; } } - diagnostics.push(TsTransfromerHelper.createDiagnostic(parentNode, 'unsupported property type intersection definition')); + context.diagnostics.push(TsTransfromerHelper.createDiagnostic(parentNode, 'unsupported property type intersection definition', context.sourceFile)); return undefined; } private extractTypeFromArrayTypeNode( parentNode: ts.Node, typeNode: ts.ArrayTypeNode, - sourceFile: ts.SourceFile, - diagnostics: ts.Diagnostic[] - ): Pick | undefined { - const itemType = this.extractTypeFromNode(parentNode, typeNode.elementType, sourceFile, diagnostics); + context: HObjectPropertyExtractContext + ): HObjectExtractPropertyTypeMeta | undefined { + const itemType = this.extractTypeFromNode(parentNode, typeNode.elementType, context); if (!itemType) { return undefined; } return { - tsType: itemType.tsType, - kind: itemType.kind, - validationRules: itemType.validationRules, + ...itemType, collectionType: CollectionType.Array, }; } @@ -139,9 +165,8 @@ export class HObjectPropertyExtractor { private extractTypeFromNode( parentNode: ts.Node, node: ts.TypeNode, - sourceFile: ts.SourceFile, - diagnostics: ts.Diagnostic[] - ): Pick | undefined { + context: HObjectPropertyExtractContext + ): HObjectExtractPropertyTypeMeta | undefined { const primitiveType = this.extractPrimitiveTypeFromNode(node); if (primitiveType) { return { @@ -153,9 +178,9 @@ export class HObjectPropertyExtractor { if (ts.isTypeReferenceNode(node)) { if (ts.isQualifiedName(node.typeName)) { - const typeName = node.typeName.getText(sourceFile); + const typeName = node.typeName.getText(context.sourceFile); if (typeName.startsWith('v.')) { - const validationRule = this.extractValidationRuleFromNode(typeName, node, sourceFile); + const validationRule = this.extractValidationRuleFromNode(typeName, node, context.sourceFile); return { kind: HObjectPropertyKind.PRIMITIVE, tsType: validationRule.primitiveType, @@ -163,14 +188,20 @@ export class HObjectPropertyExtractor { }; } } else { + const importDeclaration = context.importNameToDeclarationMap.get(node.typeName.text); + if (!importDeclaration) { + context.diagnostics.push(TsTransfromerHelper.createDiagnostic(parentNode, `Missing type import: ${node.typeName.text}`, context.sourceFile)); + } + return { kind: HObjectPropertyKind.HOBJECT, - tsType: node, + tsType: node.typeName, + tsImportDeclaration: importDeclaration }; } } - diagnostics.push(TsTransfromerHelper.createDiagnostic(parentNode, 'unsupported property type definition')); + context.diagnostics.push(TsTransfromerHelper.createDiagnostic(parentNode, 'unsupported property type definition', context.sourceFile)); return undefined; } @@ -189,7 +220,22 @@ export class HObjectPropertyExtractor { } private extractValidationRuleFromNode(typeName: string, node: ts.TypeReferenceNode, sourceFile: ts.SourceFile): ValueValidationRule { - const args = node.typeArguments ? node.typeArguments.map(arg => arg.getText(sourceFile)) : ([] as string[]); - return new ValueValidationRule(typeName, args); + const mapArgs = (arg: ts.TypeNode) => { + if (ts.isLiteralTypeNode(arg)) { + switch (arg.literal.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + return arg.literal.text; + case SyntaxKind.PrefixUnaryExpression: + return arg.literal.getText(sourceFile); + default: + throw new LogicError(`Unsupported type argument literal kind: ${SyntaxKind[arg.literal.kind]}`); + } + } + throw new LogicError(`Unsupported type argument kind: ${SyntaxKind[arg.kind]}`); + }; + + const args = node.typeArguments ? node.typeArguments.map(mapArgs) : ([] as string[]); + return new ValueValidationRule(typeName.substring(2), args); } } \ No newline at end of file diff --git a/src/Compiler/Transformer/Feature/HObject/HObjectPropertyParseTsFactory.ts b/src/Compiler/Transformer/Feature/HObject/HObjectPropertyParseTsFactory.ts index 21037e2..129411d 100644 --- a/src/Compiler/Transformer/Feature/HObject/HObjectPropertyParseTsFactory.ts +++ b/src/Compiler/Transformer/Feature/HObject/HObjectPropertyParseTsFactory.ts @@ -1,58 +1,251 @@ +import { + LogicError, + PlainParseHelper, + IntegerPlainParseHelper, + ArrayPlainParseHelper, + NumberPlainParseHelper, + StringPlainParseHelper, +} from "@hexancore/common"; import ts from "typescript"; -import { CollectionType, HObjectPropertyPrimitiveType, HObjectPropertyTsMeta } from "./HObjectPropertyTsMeta"; +import type { ImportDeclarationWrapper } from "../../Helper/ImportDeclarationWrapper"; import { TsTransfromerHelper } from "../../TsTransformerHelper"; +import { + CollectionType, + HObjectPropertyPrimitiveType, + HObjectPropertyTsMeta, + type ObjectHObjectPropertyTsMeta, + type PrimitiveHObjectPropertyTsMeta +} from "./HObjectPropertyTsMeta"; + +interface PlainParseHelperMethodCallContext { + meta: HObjectPropertyTsMeta; + helper: new () => any; + method: string; + args: ts.Expression[]; + hCommonImportDecl: ImportDeclarationWrapper; +} export class HObjectPropertyParseTsFactory { - public create(meta: HObjectPropertyTsMeta): ts.Statement[] { - if (meta.isPrimitive()) { - switch (meta.tsType) { - case HObjectPropertyPrimitiveType.string: return this.createStringParse(meta); - } - } else { - // TODO HObject parse create - return []; + public create(meta: HObjectPropertyTsMeta, hCommonImportDecl: ImportDeclarationWrapper): ts.Statement[] { + const initializer = meta.isPrimitive() + ? this.createPrimitivePropInitializer(meta, hCommonImportDecl) + : this.createHObjectPropParse(meta as ObjectHObjectPropertyTsMeta, hCommonImportDecl); + + return meta.optional + ? this.createOptionalPropertyParse(meta, initializer) + : [TsTransfromerHelper.createConstStatement(meta.name, undefined, initializer)]; + } + + private createPrimitivePropInitializer(meta: PrimitiveHObjectPropertyTsMeta, hCommonImportDecl: ImportDeclarationWrapper): ts.Expression { + switch (meta.tsType) { + case HObjectPropertyPrimitiveType.string: + return this.createStringPropParseInitializer(meta, hCommonImportDecl); + case HObjectPropertyPrimitiveType.boolean: + return this.createPrimitivePropParse({ + meta, + helper: PlainParseHelper, + method: "parseBoolean", + args: [], + hCommonImportDecl + }); + case HObjectPropertyPrimitiveType.number: + return this.createNumberPropParseInitializer(meta, hCommonImportDecl); + case HObjectPropertyPrimitiveType.bigint: + return this.createPrimitivePropParse({ + meta, + helper: PlainParseHelper, + method: "parseBigInt64", + args: [], + hCommonImportDecl + }); } - return []; - //throw new LogicError('Unspported property meta in parse(), meta: ' + meta); + throw new LogicError('Unspported property meta in parse(), meta: ' + meta); + } + + private createStringPropParseInitializer(meta: HObjectPropertyTsMeta, hCommonImportDecl: ImportDeclarationWrapper): ts.Expression { + if (meta.validationRules && !meta.validationRules[0].isCollectionItemsRule()) { + let method: string; + let args: ts.Expression[]; + const rule = meta.validationRules[0]; + switch (rule.rule) { + case "string.length": + method = "parseStringLength"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "string.length.min": + method = "parseStringLengthMin"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "string.length.max": + method = "parseStringLengthMax"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "string.length.between": + method = "parseStringLengthBetween"; + args = [this.createNumericLiteral(rule.args[0]), this.createNumericLiteral(rule.args[1])]; + break; + case "string.regex": + method = "parseStringRegex"; + // eslint-disable-next-line no-case-declarations + const pattern = rule.args[0].replace(/\\\\/g, '\\'); + // eslint-disable-next-line no-case-declarations + const flags = rule.args.length === 2 ? rule.args[1] : ''; + // eslint-disable-next-line no-case-declarations + const regex = `/${pattern}/${flags}`; + + args = [ts.factory.createRegularExpressionLiteral(regex)]; + break; + default: + throw new LogicError("Unsupported property validation rule in parse(), meta: " + meta); + } + + return this.createPrimitivePropParse({ + meta, + helper: StringPlainParseHelper, + method, + args, + hCommonImportDecl, + }); + } + return this.createPrimitivePropParse({ + meta, + helper: StringPlainParseHelper, + method: "parseString", + args: [], + hCommonImportDecl, + }); } - private createStringParse(meta: HObjectPropertyTsMeta): ts.Statement[] { - /* - let initializer; - if (meta.validationRules) { - if (meta.validationRules[0].isStringLengthRule()) { - switch (meta.validationRules[0].extraRuleParts.join('.')) { - case "length": - initializer = this.createPrimitivePropertyParseCode(meta, "parseStringLength", { - scalar: meta.validationRules[0].args[0], - }); + private createNumberPropParseInitializer(meta: PrimitiveHObjectPropertyTsMeta, hCommonImportDecl: ImportDeclarationWrapper): ts.Expression { + if (meta.validationRules && !meta.validationRules[0].isCollectionItemsRule()) { + let method: string; + let args: ts.Expression[]; + const rule = meta.validationRules[0]; + switch (rule.rule) { + case "int": + method = "parseInt"; + args = []; + break; + + case "int.min": + method = "parseIntGTE"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "int.max": + method = "parseIntLTE"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "int.between": + method = "parseIntBetween"; + args = [this.createNumericLiteral(rule.args[0]), this.createNumericLiteral(rule.args[1])]; + break; + + case "int.gt": + method = "parseIntGT"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "int.lt": + method = "parseIntLT"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "int.between_exclusively": + method = "parseIntBetweenExclusively"; + args = [this.createNumericLiteral(rule.args[0]), this.createNumericLiteral(rule.args[1])]; + break; + + case "uint": + method = "parseUInt"; + args = []; + break; + case "uint.min": + method = "parseUIntGTE"; + args = [this.createNumericLiteral(rule.args[0])]; break; - case "length.min": - initializer = + case "uint.max": + method = "parseUIntLTE"; + args = [this.createNumericLiteral(rule.args[0])]; break; - case "length.max": - initializer = + case "uint.between": + method = "parseUIntBetween"; + args = [this.createNumericLiteral(rule.args[0]), this.createNumericLiteral(rule.args[1])]; break; - } + + case "uint.gt": + method = "parseUIntGT"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "uint.lt": + method = "parseUIntLT"; + args = [this.createNumericLiteral(rule.args[0])]; + break; + case "uint.between_exclusively": + method = "parseUIntBetweenExclusively"; + args = [this.createNumericLiteral(rule.args[0]), this.createNumericLiteral(rule.args[1])]; + break; + + default: + throw new LogicError("Unsupported property validation rule in parse(), meta: " + meta); } - } else { - initializer = this.createPrimitivePropertyParseCode(meta, "parseString"); - }* */ - const initializer = this.createPrimitivePropertyParseCode(meta, "parseString"); - return meta.optional - ? this.createOptionalPropertyParse(meta, initializer) - : [TsTransfromerHelper.createConstStatement(meta.name, undefined, initializer)]; + return this.createPrimitivePropParse({ + meta, + helper: IntegerPlainParseHelper, + method, + args, + hCommonImportDecl, + }); + } + + return this.createPrimitivePropParse({ + meta, + helper: NumberPlainParseHelper, + method: "parseNumber", + args: [], + hCommonImportDecl, + }); } - private createPrimitivePropertyParseCode(meta: HObjectPropertyTsMeta, helperMethod: string): ts.Expression { + private createPrimitivePropParse(c: PlainParseHelperMethodCallContext): ts.Expression { + const meta = c.meta; if (meta.collectionType === CollectionType.Array) { - return this.createParsePrimitiveArray(meta, helperMethod); - } else { - return this.createPlainParseHelperCall(meta, helperMethod); + const parseFn = ts.factory.createArrowFunction(undefined, undefined, + [ts.factory.createParameterDeclaration(undefined, undefined, "pi")], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + this.createPlainParseHelperCall({ + meta: c.meta, + helper: c.helper, + method: c.method, + args: [ts.factory.createIdentifier("pi"), ...c.args], + hCommonImportDecl: c.hCommonImportDecl + }, false)); + + return this.createParsePrimitiveArray(c.meta, parseFn, c.hCommonImportDecl); } + + return this.createPlainParseHelperCall({ + meta: c.meta, + helper: c.helper, + method: c.method, + args: [this.createPlainObjPropertyAccess(c.meta), ...c.args], + hCommonImportDecl: c.hCommonImportDecl + }); + } + + private createHObjectPropParse(meta: ObjectHObjectPropertyTsMeta, hCommonImportDecl: ImportDeclarationWrapper): ts.Expression { + if (meta.collectionType === CollectionType.Array) { + return this.createHObjectArrayPropParse(meta, hCommonImportDecl); + } + + return this.createPlainParseHelperCall({ + meta, + helper: PlainParseHelper, + method: "parseHObject", + args: [this.createPlainObjPropertyAccess(meta), meta.createTypeIdentifier()], + hCommonImportDecl + }); } private createOptionalPropertyParse(meta: HObjectPropertyTsMeta, initializer: ts.Expression): ts.Statement[] { @@ -61,7 +254,7 @@ export class HObjectPropertyParseTsFactory { const ifCondition = ts.factory.createBinaryExpression( this.createPlainObjPropertyAccess(meta), ts.factory.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken), - ts.factory.createIdentifier('undefined') + ts.factory.createIdentifier("undefined") ); const optionalPropertyIf = ts.factory.createIfStatement( @@ -81,31 +274,55 @@ export class HObjectPropertyParseTsFactory { ); } - private createParsePrimitiveArray(meta: HObjectPropertyTsMeta, parse: ts.Expression | string) { - parse = typeof parse === 'string' ? this.createPlainParseHelperPropertyAccess(parse) : parse; - return this.createPlainParseHelperCall(meta, "parsePrimitiveArray", [parse]); + private createParsePrimitiveArray(meta: HObjectPropertyTsMeta, parseFn: ts.Expression, hCommonImportDecl: ImportDeclarationWrapper) { + return this.createArrayPropParse(meta, "parsePrimitiveArray", [parseFn], hCommonImportDecl); + } + + private createHObjectArrayPropParse(meta: ObjectHObjectPropertyTsMeta, hCommonImportDecl: ImportDeclarationWrapper) { + return this.createArrayPropParse(meta, "parseHObjectArray", [meta.createTypeIdentifier()], hCommonImportDecl); + } + + private createArrayPropParse(meta: HObjectPropertyTsMeta, method: string, extraArgs: ts.Expression[], hCommonImportDecl: ImportDeclarationWrapper): ts.Expression { + const itemsRule = meta.getItemsRule(); + const args: ts.Expression[] = [this.createPlainObjPropertyAccess(meta)]; + if (itemsRule) { + args.push(...itemsRule.args.map(a => this.createNumericLiteral(a))); + const itemsRuleType = itemsRule.ruleParts[1]; + method = method + "Items" + itemsRuleType[0].toUpperCase() + itemsRuleType.slice(1); + } + + args.push(...extraArgs); + return this.createPlainParseHelperCall({ meta, helper: ArrayPlainParseHelper, method, args, hCommonImportDecl }); } - private createPlainParseHelperCall(meta: HObjectPropertyTsMeta, method: string, args: ts.Expression[] = []) { + // .(args) + private createPlainParseHelperCall(c: PlainParseHelperMethodCallContext, appendPathAndIssuesArgs = true) { + const helperClassIdentifier = ts.factory.createIdentifier(c.helper.name); + let args = c.args; + if (appendPathAndIssuesArgs) { + args = [ + ...c.args, + c.meta.createNameStringLiteral(), + ts.factory.createIdentifier("issues") + ]; + } + return ts.factory.createCallExpression( - this.createPlainParseHelperPropertyAccess(method), + this.createPlainParseHelperPropertyAccess(helperClassIdentifier, c.method), undefined, - [ - this.createPlainObjPropertyAccess(meta), - ...args, - ts.factory.createStringLiteral(meta.name), - ts.factory.createIdentifier('issues') - ] + args ); } - private createPlainParseHelperPropertyAccess(name: string): ts.PropertyAccessExpression { + // . + private createPlainParseHelperPropertyAccess(helper: ts.Identifier | ts.PropertyAccessChain, name: string): ts.PropertyAccessExpression { return ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier("PlainParseHelper"), + helper, ts.factory.createIdentifier(name) ); } + // p. private createPlainObjPropertyAccess(meta: HObjectPropertyTsMeta): ts.PropertyAccessExpression { return ts.factory.createPropertyAccessExpression( this.createPlainObjIdentifier(), @@ -117,6 +334,14 @@ export class HObjectPropertyParseTsFactory { return ts.factory.createIdentifier("p"); } + private createNumericLiteral(num: string): ts.Expression { + if (Number(num) >= 0) { + return ts.factory.createNumericLiteral(num); + } - + return ts.factory.createPrefixUnaryExpression( + ts.SyntaxKind.MinusToken, + ts.factory.createNumericLiteral(num.substring(1)) + ); + } } \ No newline at end of file diff --git a/src/Compiler/Transformer/Feature/HObject/HObjectPropertyTsMeta.ts b/src/Compiler/Transformer/Feature/HObject/HObjectPropertyTsMeta.ts index d8905c5..43de42c 100644 --- a/src/Compiler/Transformer/Feature/HObject/HObjectPropertyTsMeta.ts +++ b/src/Compiler/Transformer/Feature/HObject/HObjectPropertyTsMeta.ts @@ -1,5 +1,6 @@ import ts from "typescript"; import type { ImportDeclarationWrapper } from "../../Helper/ImportDeclarationWrapper"; +import type { JsonSerialize } from "@hexancore/common"; export enum HObjectPropertyKind { PRIMITIVE = 0, @@ -14,17 +15,16 @@ export enum HObjectPropertyPrimitiveType { bigint = 4, } -export type HObjectPropertyType = HObjectPropertyPrimitiveType | ts.TypeReferenceNode | ts.Identifier; +export type HObjectPropertyType = HObjectPropertyPrimitiveType | ts.Identifier; export class ValueValidationRule { - public extraRuleParts: string[]; + public ruleParts: string[]; public primitiveType: HObjectPropertyPrimitiveType; public constructor(public rule: string, public args: string[]) { const parts = rule.split('.'); - parts.shift(); // remove `v` this.primitiveType = this.parseRulePrimitiveType(parts); - this.extraRuleParts = parts; + this.ruleParts = parts; } private parseRulePrimitiveType(parts: string[]) { @@ -42,16 +42,50 @@ export class ValueValidationRule { return type; } - parts.shift(); return type; } public isCollectionItemsRule(): boolean { - return this.extraRuleParts[0] === "items"; + return this.ruleParts[0] === "items"; } - public isStringLengthRule(): boolean { - return this.extraRuleParts[1] === "length"; + // string.regex + public isStringRegexRule(): boolean { + return (this.ruleParts.length === 2 && this.ruleParts[1] === "regex"); + } + + // string.length + public isStringOnlyLengthRule(): boolean { + return this.ruleParts.length === 2 && this.ruleParts[1] === "length"; + } + + // string.length.rule + public isStringLengthRule(rule?: | "min" | "max" | "between"): boolean { + return (!rule && this.ruleParts.length === 2) || (this.ruleParts.length === 3 && this.ruleParts[2] === rule); + } + + public isOnlyIntRule(): boolean { + return this.ruleParts.length === 1 && this.ruleParts[0] === "int"; + } + + public isIntRule(rule: "min" | "max" | "between" | "between_exclusive" | "gt" | "lt"): boolean { + return this.ruleParts.length === 2 && this.ruleParts[0] === "int" && this.ruleParts[1] === rule; + } + + public isOnlyUIntRule(): boolean { + return this.ruleParts.length === 1 && this.ruleParts[0] === "uint"; + } + + public isUIntRule(rule: "min" | "max" | "between" | "between_exclusive" | "gt" | "lt"): boolean { + return this.ruleParts.length === 2 && this.ruleParts[0] === "uint" && this.ruleParts[1] === rule; + } + + public isOnlyFloatRule(): boolean { + return this.ruleParts.length === 1 && this.ruleParts[0] === "float"; + } + + public isFloatRule(rule: "min" | "max" | "between" | "between_exclusive" | "gt" | "lt"): boolean { + return this.ruleParts.length === 2 && this.ruleParts[0] === "float" && this.ruleParts[1] === rule; } public toString(): string { @@ -66,119 +100,93 @@ export enum CollectionType { Set = 'Set', } -export interface PrimitiveHObjectPropertyTsMeta { - kind: HObjectPropertyKind.PRIMITIVE, - name: string, - tsType: HObjectPropertyPrimitiveType, - optional: boolean, - validationRules?: ValueValidationRule[], - collectionType?: CollectionType, -} - -export interface ObjectHObjectPropertyTsMeta { - kind: HObjectPropertyKind.HOBJECT, - name: string, - tsType: ts.TypeReferenceNode | ts.Identifier, - tsImportDeclaration: ImportDeclarationWrapper, - optional: boolean, - collectionType?: CollectionType, -} - -export class HObjectPropertyTsMeta { +export abstract class HObjectPropertyTsMeta implements JsonSerialize { public constructor( - public kind: HObjectPropertyKind, - public name: string, - public tsType: HObjectPropertyType, - public optional: boolean, - public validationRules?: ValueValidationRule[], - public collectionType?: CollectionType, - public tsImportDeclaration?: ImportDeclarationWrapper, + public readonly name: string, + public readonly optional: boolean, + public readonly validationRules?: ValueValidationRule[], + public readonly collectionType?: CollectionType, ) { } - public static createWithValidationRule( - name: string, - optional: boolean, - validationRules: ValueValidationRule[], - collectionType?: CollectionType - ): HObjectPropertyTsMeta { - const primitiveType = validationRules[0].primitiveType; - return new this(HObjectPropertyKind.PRIMITIVE, name, primitiveType, optional, validationRules, collectionType); + public isPrimitive(): this is PrimitiveHObjectPropertyTsMeta { + return this instanceof PrimitiveHObjectPropertyTsMeta; } - public static createString(name: string, optional: boolean, collectionType?: CollectionType, itemsValidationRule?: ValueValidationRule): HObjectPropertyTsMeta { - return new this( - HObjectPropertyKind.PRIMITIVE, - name, - HObjectPropertyPrimitiveType.string, - optional, - itemsValidationRule ? [itemsValidationRule] : [], - collectionType - ); + public isHObject(): this is ObjectHObjectPropertyTsMeta { + return this instanceof ObjectHObjectPropertyTsMeta; } - public static createNumber( - name: string, - optional: boolean, - collectionType?: CollectionType, - itemsValidationRule?: ValueValidationRule, - ): HObjectPropertyTsMeta { - return new this( - HObjectPropertyKind.PRIMITIVE, - name, - HObjectPropertyPrimitiveType.number, - optional, - itemsValidationRule ? [itemsValidationRule] : [], - collectionType - ); - } - - public static createBoolean( - name: string, - optional: boolean, - collectionType?: CollectionType, - itemsValidationRule?: ValueValidationRule, - ): HObjectPropertyTsMeta { - return new this( - HObjectPropertyKind.PRIMITIVE, - name, - HObjectPropertyPrimitiveType.boolean, - optional, - itemsValidationRule ? [itemsValidationRule] : [], - collectionType - ); - } - - public static createBigInt( + public getItemsRule(): ValueValidationRule | undefined { + return this.validationRules?.find(r => r.isCollectionItemsRule()); + } + + public createNameStringLiteral(): ts.StringLiteral { + return ts.factory.createStringLiteral(this.name); + } + + protected formatCollectionTypeToString(type: string): string { + if (this.collectionType === CollectionType.Array) { + return type + "[]"; + } + + return type; + } + + public abstract toJSON(): any; +} + +export class ObjectHObjectPropertyTsMeta extends HObjectPropertyTsMeta { + public constructor( name: string, + public readonly tsType: ts.Identifier, + public readonly tsImportDeclaration: ImportDeclarationWrapper, optional: boolean, + validationRules?: ValueValidationRule[], collectionType?: CollectionType, - itemsValidationRule?: ValueValidationRule, - ): HObjectPropertyTsMeta { - return new this( - HObjectPropertyKind.PRIMITIVE, - name, - HObjectPropertyPrimitiveType.bigint, - optional, - itemsValidationRule ? [itemsValidationRule] : [], - collectionType - ); + ) { + super(name, optional, validationRules, collectionType); } - public isPrimitive(): this is PrimitiveHObjectPropertyTsMeta { - return this.kind === HObjectPropertyKind.PRIMITIVE; + public createTypeIdentifier(): ts.PropertyAccessChain | ts.Identifier { + return this.tsImportDeclaration.get(this.tsType.text); } - public isHObject(): this is ObjectHObjectPropertyTsMeta { - return this.kind === HObjectPropertyKind.HOBJECT; + public toJSON(): any { + return { + kind: HObjectPropertyKind.HOBJECT, + name: this.name, + type: this.tsType.getText(), + optional: this.optional, + validationRules: this.validationRules, + collectionType: this.collectionType, + }; + } + + public toString(): string { + let type = (this.tsType as ts.TypeReferenceNode | ts.Identifier).getText(); + type = this.formatCollectionTypeToString(type); + return `${this.name}${this.optional ? '?' : '!'}: ${type}`; + } +} + +export class PrimitiveHObjectPropertyTsMeta extends HObjectPropertyTsMeta { + public constructor( + name: string, + public readonly tsType: HObjectPropertyPrimitiveType, + optional: boolean, + validationRules?: ValueValidationRule[], + collectionType?: CollectionType, + ) { + super(name, optional, validationRules, collectionType); } public toJSON(): any { return { - kind: HObjectPropertyKind[this.kind], + kind: HObjectPropertyKind.PRIMITIVE, name: this.name, - type: this.kind === HObjectPropertyKind.PRIMITIVE ? HObjectPropertyPrimitiveType[this.tsType as HObjectPropertyPrimitiveType] : this.tsType, + type: HObjectPropertyPrimitiveType[this.tsType as HObjectPropertyPrimitiveType], optional: this.optional, validationRules: this.validationRules, collectionType: this.collectionType, @@ -186,13 +194,7 @@ export class HObjectPropertyTsMeta { } public toString(): string { - let type: string; - if (this.kind === HObjectPropertyKind.PRIMITIVE) { - type = this.formatPrimitiveTypeToString(); - } else { - type = (this.tsType as ts.TypeReferenceNode | ts.Identifier).getText(); - type = this.formatCollectionTypeToString(type); - } + const type = this.formatPrimitiveTypeToString(); return `${this.name}${this.optional ? '?' : '!'}: ${type}`; } @@ -211,15 +213,5 @@ export class HObjectPropertyTsMeta { } else { return this.formatCollectionTypeToString(HObjectPropertyPrimitiveType[this.tsType as any]); } - - - } - - private formatCollectionTypeToString(type: string): string { - if (this.collectionType === CollectionType.Array) { - return type + "[]"; - } - - return type; } } \ No newline at end of file diff --git a/src/Compiler/Transformer/Feature/HObject/HObjectToJSONTsFactory.ts b/src/Compiler/Transformer/Feature/HObject/HObjectToJSONTsFactory.ts index 344e240..521414d 100644 --- a/src/Compiler/Transformer/Feature/HObject/HObjectToJSONTsFactory.ts +++ b/src/Compiler/Transformer/Feature/HObject/HObjectToJSONTsFactory.ts @@ -10,7 +10,7 @@ export class HObjectToJSONTsFactory { ); return ts.factory.createMethodDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword), ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)], + [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], undefined, // * 'toJSON', undefined, // '?' @@ -52,12 +52,12 @@ export class HObjectToJSONTsFactory { if (p.collectionType === CollectionType.Array) { return this.createMethodOfPropCall( ts.factory.createThis(), p.name, 'map', - this.createArrayMapCallArgs(this.createMethodCall('item', 'toJSON', []),), + this.createArrayMapCallArgs(this.createMethodCall('item', 'toJSON', [])), p.optional ); } - return ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createIdentifier(p.name)); + return this.createMethodOfPropCall(ts.factory.createThis(), p.name, 'toJSON', [], p.optional); } private createArrayMapCallArgs(body: ts.ConciseBody, itemVarName = 'item') { @@ -74,15 +74,13 @@ export class HObjectToJSONTsFactory { } private createMethodOfPropCall(objExp: ts.Expression, prop: string, method: string, args: ts.Expression[], optional: boolean): ts.CallExpression { - return ts.factory.createCallChain( - ts.factory.createPropertyAccessExpression( - ts.factory.createPropertyAccessExpression(objExp, ts.factory.createIdentifier(prop)), - ts.factory.createIdentifier(method) - ), - optional ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) : undefined, - undefined, - args + const questionDot = optional ? ts.factory.createToken(ts.SyntaxKind.QuestionDotToken) : undefined; + const objPropMethodAccess = ts.factory.createPropertyAccessChain( + ts.factory.createPropertyAccessExpression(objExp, prop), + questionDot, + method ); + return ts.factory.createCallChain(objPropMethodAccess, undefined, undefined, args); } private createMethodCall(varName: string, method: string, args: ts.Expression[]): ts.CallExpression { diff --git a/src/Compiler/Transformer/Feature/HObject/HObjectTsTransformer.ts b/src/Compiler/Transformer/Feature/HObject/HObjectTsTransformer.ts index 00492de..886a80f 100644 --- a/src/Compiler/Transformer/Feature/HObject/HObjectTsTransformer.ts +++ b/src/Compiler/Transformer/Feature/HObject/HObjectTsTransformer.ts @@ -8,27 +8,32 @@ import { HObjectToJSONTsFactory } from "./HObjectToJSONTsFactory"; import type { FeatureSourcePath } from "../../../../Util/Feature/FeatureModuleDiscoverer"; import { HObjectToConstructorTsFactory } from "./HObjectConstructorTsFactory"; import { ImportDeclarationWrapper } from "../../Helper/ImportDeclarationWrapper"; -import { HObjectKind } from "@/Util/Feature/Meta"; +import { HObjectKind, type FeatureHObjectMeta } from "@/Util/Feature/Meta"; interface VisitContext extends FeatureTransformContext { source: ts.SourceFile; importNameToDeclarationMap: Map; hCommonImportDecl?: ImportDeclarationWrapper; + meta: FeatureHObjectMeta; } const HObjectHCommonImports = [ 'R', 'OK', - 'type JsonObjectType', - 'type PlainParsableHObjectType', - 'type PlainParseError', 'PlainParseHelper', - 'InvalidTypePlainParseIssue', + 'IntegerPlainParseHelper', + 'ArrayPlainParseHelper', + 'StringPlainParseHelper', + 'NumberPlainParseHelper', 'HObjectTypeMeta', - 'PlainParseIssue', - 'TooBigPlainParseIssue', - 'TooSmallPlainParseIssue' ]; +const HObjectHCommonTypeOnlyImports = [ + 'JsonObjectType', + 'PlainParseError', + 'PlainParseIssue' +]; + +const GENERATED_METHODS_NAMES = ["parse", "toJSON"]; export class HObjectTsTransformer extends AbstractFeatureTsTransformer { @@ -60,6 +65,7 @@ export class HObjectTsTransformer extends AbstractFeatureTsTransformer { ...context, importNameToDeclarationMap: new Map(), source, + meta: context.feature.hObjectMap.get(context.featureSourcePath.localSourcePath)!, }; const transformed = ts.factory.updateSourceFile(source, ts.visitNodes( @@ -96,7 +102,7 @@ export class HObjectTsTransformer extends AbstractFeatureTsTransformer { if (this.isHexancoreCommonImport(visitContext, node)) { visitContext.hCommonImportDecl = wrapper; - return this.extendHexancoreCommonImportDecl(visitContext.hCommonImportDecl); + this.extendHexancoreCommonImportDecl(visitContext); } wrapper.importNames.forEach(name => { @@ -111,13 +117,24 @@ export class HObjectTsTransformer extends AbstractFeatureTsTransformer { return !visitContext.hCommonImportDecl && ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@hexancore/common'; } - private extendHexancoreCommonImportDecl(decl: ImportDeclarationWrapper): ts.ImportDeclaration { - const newImports = HObjectHCommonImports.filter(i => !decl.hasNamedAccess(i)); - return decl.addNamedImports(newImports); + private extendHexancoreCommonImportDecl(visitContext: VisitContext): void { + const decl: ImportDeclarationWrapper = visitContext.hCommonImportDecl!; + + const newImports = [ + ...HObjectHCommonImports.filter(i => !decl.hasNamedAccess(i)), + ...HObjectHCommonTypeOnlyImports.filter(i => !decl.hasNamedAccess(i)).map(name => ts.factory.createImportSpecifier(true, undefined, ts.factory.createIdentifier(name))), + ts.factory.createImportSpecifier(true, undefined, ts.factory.createIdentifier("Any" + visitContext.meta.kind)), + ts.factory.createImportSpecifier(true, undefined, ts.factory.createIdentifier(visitContext.meta.kind + "Type")), + ]; + decl.addNamedImports(newImports); } private updateClassDeclaration(node: ts.ClassDeclaration, visitContext: Required) { - const properties = this.propertyExtractor.extract(node, visitContext.source, visitContext.diagnostics); + const properties = this.propertyExtractor.extract(node, { + sourceFile: visitContext.source, + diagnostics: visitContext.diagnostics, + importNameToDeclarationMap: visitContext.importNameToDeclarationMap + }); const hObjectClassName = node.name!.text; const newMembers = [ @@ -126,9 +143,21 @@ export class HObjectTsTransformer extends AbstractFeatureTsTransformer { ]; if (properties.length > 0) { - newMembers.push(this.constructorTsFactory.create(properties)); - newMembers.push(this.parseTsFactory.create(hObjectClassName, properties, visitContext.hCommonImportDecl)); - newMembers.push(this.toJSONTsFactory.create(hObjectClassName, properties, visitContext.hCommonImportDecl)); + const currentGeneratedMethods = node.members.filter( + m => ts.isConstructorDeclaration(node) || (ts.isMethodDeclaration(m) && GENERATED_METHODS_NAMES.includes(m.name.getText(visitContext.source))) + ).map(m => m.name!.getText(visitContext.source)); + + if (!currentGeneratedMethods.includes('constructor')) { + newMembers.push(this.constructorTsFactory.create(properties)); + } + + if (!currentGeneratedMethods.includes('parse')) { + newMembers.push(this.parseTsFactory.create(visitContext.meta, hObjectClassName, properties, visitContext.hCommonImportDecl)); + } + + if (!currentGeneratedMethods.includes('toJSON')) { + newMembers.push(this.toJSONTsFactory.create(hObjectClassName, properties, visitContext.hCommonImportDecl)); + } } return ts.factory.updateClassDeclaration( @@ -155,7 +184,7 @@ export class HObjectTsTransformer extends AbstractFeatureTsTransformer { const hobjectTypeMetaCreateExp = ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier("HObjectTypeMeta"), - ts.factory.createIdentifier(featureHObjMeta.layer) + ts.factory.createIdentifier(featureHObjMeta.layer.toLowerCase()) ), undefined, hObjTypeMetaCreateParameters diff --git a/src/Compiler/Transformer/Helper/ImportDeclarationWrapper.ts b/src/Compiler/Transformer/Helper/ImportDeclarationWrapper.ts index 5255d50..f80290f 100644 --- a/src/Compiler/Transformer/Helper/ImportDeclarationWrapper.ts +++ b/src/Compiler/Transformer/Helper/ImportDeclarationWrapper.ts @@ -18,7 +18,7 @@ export class ImportDeclarationWrapper { } public get(name: string): ts.PropertyAccessChain | ts.Identifier { - return TsTransfromerHelper.createNamedImportAccess(this.declaration, name, this.context); + return ts.factory.createIdentifier(name); //TsTransfromerHelper.createNamedImportAccess(this.declaration, name, this.context); } public hasNamedAccess(name: string): boolean { diff --git a/src/Compiler/Transformer/TsTransformerHelper.ts b/src/Compiler/Transformer/TsTransformerHelper.ts index d1b8589..29ac463 100644 --- a/src/Compiler/Transformer/TsTransformerHelper.ts +++ b/src/Compiler/Transformer/TsTransformerHelper.ts @@ -191,11 +191,11 @@ export class TsTransfromerHelper { ); } - public static createDiagnostic(node: ts.Node, message: string): ts.Diagnostic { + public static createDiagnostic(node: ts.Node, message: string, source: ts.SourceFile): ts.Diagnostic { return { - file: node.getSourceFile(), - start: node.getStart(), - length: node.getWidth(), + file: source, + start: node.getStart(source), + length: node.getWidth(source), messageText: message, category: ts.DiagnosticCategory.Error, code: 9001 diff --git a/src/Infrastructure/Persistence/Domain/Generic/Persister/AbstractEntityPersister.ts b/src/Infrastructure/Persistence/Domain/Generic/Persister/AbstractEntityPersister.ts index 60765d9..7cf6df7 100644 --- a/src/Infrastructure/Persistence/Domain/Generic/Persister/AbstractEntityPersister.ts +++ b/src/Infrastructure/Persistence/Domain/Generic/Persister/AbstractEntityPersister.ts @@ -7,7 +7,7 @@ import { DomainErrors, OK, EnumEntityErrorTypeWrapper, - DateTime, + HDateTime, OKA, isIterable, } from '@hexancore/common'; @@ -27,13 +27,13 @@ export interface GetQueryOptions { export class EntityPropertiesNowInjector> { public constructor(private properties: string[]) {} - public inject(now: DateTime, entity: T): void { + public inject(now: HDateTime, entity: T): void { this.properties.forEach((propertyName: string) => { entity[propertyName] = entity[propertyName] ?? now; }); } - public injectMany(now: DateTime, entities: T[]): void { + public injectMany(now: HDateTime, entities: T[]): void { this.properties.forEach((propertyName: string) => { entities.forEach((e) => { e[propertyName] = e[propertyName] ?? now; diff --git a/src/Util/Feature/Meta/FeatureApplicationMeta.ts b/src/Util/Feature/Meta/FeatureApplicationMeta.ts index 81f4d34..c877b42 100644 --- a/src/Util/Feature/Meta/FeatureApplicationMeta.ts +++ b/src/Util/Feature/Meta/FeatureApplicationMeta.ts @@ -33,7 +33,7 @@ export class FeatureApplicationCommandMeta implements FeatureApplicationMessageM } public get layer(): HFeatureBackendLayer { - return 'application'; + return 'Application'; } public get hashData(): string { @@ -78,7 +78,7 @@ export class FeatureApplicationQueryMeta implements FeatureApplicationMessageMet } public get layer(): HFeatureBackendLayer { - return 'application'; + return 'Application'; } public get hashData(): string { diff --git a/src/Util/Feature/Meta/FeatureDomainMeta.ts b/src/Util/Feature/Meta/FeatureDomainMeta.ts index 1671e17..435f2db 100644 --- a/src/Util/Feature/Meta/FeatureDomainMeta.ts +++ b/src/Util/Feature/Meta/FeatureDomainMeta.ts @@ -43,7 +43,7 @@ export class FeatureAggregateRootMeta implements FeatureHObjectMeta { } public get layer(): HFeatureBackendLayer { - return 'domain'; + return 'Domain'; } public get hashData(): string { @@ -97,7 +97,7 @@ export class FeatureEntityMeta implements FeatureHObjectMeta { } public get layer(): HFeatureBackendLayer { - return 'domain'; + return 'Domain'; } public get hashData(): string { @@ -150,7 +150,7 @@ export class FeatureValueObjectMeta implements FeatureHObjectMeta { } public get layer(): HFeatureBackendLayer { - return 'domain'; + return 'Domain'; } public toJSON(): any { diff --git a/test/helper/FeatureTsTransformerTestHelper.ts b/test/helper/FeatureTsTransformerTestHelper.ts new file mode 100644 index 0000000..182f2fd --- /dev/null +++ b/test/helper/FeatureTsTransformerTestHelper.ts @@ -0,0 +1,57 @@ +import { TsTransformerTestHelper } from "@/Compiler/Test/TsTransformerTestHelper"; +import { FeatureTsTransformer } from "@/Compiler/Transformer/Feature/FeatureTsTransformer"; +import type { TransformerFactory, SourceFile } from "typescript"; +import { LIBS_DIRNAME } from "./libs"; +import { ESLint } from "eslint"; + +// EXPERIMENTAL: check generated code linting +async function lint(code: string) { + const eslint = new ESLint({ + baseConfig: { + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + env: { node: true, es6: true }, + globals: { + common_1: "readonly", + }, + rules: { + "semi": ["error", "always"], + }, + }, + }); + + const results = await eslint.lintText(code); + + const formatter = await eslint.loadFormatter("stylish"); + return results[0].output; +} + +export class FeatureTsTransformerTestHelper { + private constructor( + private transformHelper: TsTransformerTestHelper, + private transformerFactory: TransformerFactory, + private sourceRoot: string + + ) { + + } + + public static create(): FeatureTsTransformerTestHelper { + const tsConfigFilePath = process.cwd() + "/tsconfig.json"; + const helper = TsTransformerTestHelper.createFromTsConfig(tsConfigFilePath); + + const sourceRoot = LIBS_DIRNAME + "/test-lib/src"; + const compilerRoot = process.cwd() + "/lib/Compiler"; + const transformer = FeatureTsTransformer.create(sourceRoot, compilerRoot); + + const transformerFactory = helper.createTransformerFactory(transformer.transform.bind(transformer)); + + return new FeatureTsTransformerTestHelper(helper, transformerFactory, sourceRoot); + + } + + public transform(sourceFilePath: string): string { + return this.transformHelper.transformExistingAndReturnAsString(this.sourceRoot + "/" + sourceFilePath, [this.transformerFactory]); + } + +} \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/ArrayItemsTestDto.ts b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/ArrayItemsTestDto.ts new file mode 100644 index 0000000..a761f4c --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/ArrayItemsTestDto.ts @@ -0,0 +1,11 @@ +import { Dto, v } from '@hexancore/common'; + +export class ArrayItemsTestDto extends Dto { + + public arrayMinItemsField!: v.int[] & v.items.min<2>; + public arrayMaxItemsField!: v.int[] & v.items.max<2>; + public arrayExaclyItemsField!: v.int[] & v.items.exactly<2>; + public arrayBetweenItemsField!: v.int[] & v.items.between<0, 2>; + + public optionalArrayItemsField?: v.int[] & v.items.min<2>; +} \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/BookDto.ts b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/BookDto.ts index fdc93e6..e43ee9f 100644 --- a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/BookDto.ts +++ b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/BookDto.ts @@ -1,5 +1,5 @@ import { Dto } from "@hexancore/common"; -export class BookDto extends Dto { +export class BookDto extends Dto { public title?: string; } \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/FloatTestDto.ts b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/FloatTestDto.ts new file mode 100644 index 0000000..ffc67e5 --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/FloatTestDto.ts @@ -0,0 +1,23 @@ +import { Dto, v } from '@hexancore/common'; + +export class FloatTestDto extends Dto { + + public field!: v.float; + public optionalField?: v.float; + + public minField!: v.float.min<-100>; + public maxField!: v.float.max<100>; + + public betweenField!: v.float.between<-100, 100>; + + public gtField!: v.float.gt<-100>; + public ltField!: v.float.lt<100>; + + public betweenExclusivelyField!: v.float.between_exclusively<-100, 100>; + + public arrayField!: v.float[]; + public optionalArrayField?: v.float[]; + + public maxArrayField!: v.float.max<100>[]; + +} \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/IntTestDto.ts b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/IntTestDto.ts new file mode 100644 index 0000000..ba1b6dc --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/IntTestDto.ts @@ -0,0 +1,23 @@ +import { Dto, v } from '@hexancore/common'; + +export class IntTestDto extends Dto { + + public field!: v.int; + public optionalField?: v.int; + + public minField!: v.int.min<-100>; + public maxField!: v.int.max<100>; + + public betweenField!: v.int.between<-100, 100>; + + public gtField!: v.int.gt<-100>; + public ltField!: v.int.lt<100>; + + public betweenExclusivelyField!: v.int.between_exclusively<-100, 100>; + + public arrayField!: v.int[]; + public optionalArrayField?: v.int[]; + + public maxArrayField!: v.int.max<100>[]; + +} \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/StringTestDto.ts b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/StringTestDto.ts new file mode 100644 index 0000000..c62aebc --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/StringTestDto.ts @@ -0,0 +1,17 @@ +import { Dto, v } from '@hexancore/common'; + +export class StringTestDto extends Dto { + + public field!: string; + public optionalField?: string; + + public lengthField!: v.string.length<5>; + public lengthMinField!: v.string.length.min<5>; + public lengthMaxField!: v.string.length.max<20>; + public lengthBetweenField!: v.string.length.between<5, 20>; + + public regexField!: v.string.regex<'[a-z]{2}\\d{3}'>; + + public arrayField!: v.string.length.min<5>[]; + public optionalArrayField?: v.string.length.min<5>[]; +} \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/TestTransformDto.ts b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/TestTransformDto.ts index 7b1be33..ff6f6ae 100644 --- a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/TestTransformDto.ts +++ b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/TestTransformDto.ts @@ -1,8 +1,8 @@ import { Dto, v, RefId } from '@hexancore/common'; -export class TestTransformDto extends Dto { +export class TestTransformDto extends Dto { - public optionalField?: string; + public optionalStringField?: string; public numberField!: number; public stringField!: string; @@ -19,5 +19,6 @@ export class TestTransformDto extends Dto { public ruleArrayWithItemsField!: v.int.between<-10, 100>[] & v.items.between<2, 5>; public hObjField!: RefId; + public optionalHObjField?: RefId; public hObjArrayField!: RefId[]; -} \ No newline at end of file +} diff --git a/test/helper/libs/test-lib/src/Book/Application/Book/Dto/UIntTestDto.ts b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/UIntTestDto.ts new file mode 100644 index 0000000..cd29b36 --- /dev/null +++ b/test/helper/libs/test-lib/src/Book/Application/Book/Dto/UIntTestDto.ts @@ -0,0 +1,21 @@ +import { Dto, v } from '@hexancore/common'; + +export class UIntTestDto extends Dto { + + public field!: v.uint; + public optionalField?: v.uint; + + public minField!: v.uint.min<0>; + public maxField!: v.uint.max<100>; + public betweenField!: v.uint.between<0, 100>; + + public gtField!: v.uint.gt<0>; + public ltField!: v.uint.lt<100>; + public betweenExclusivelyField!: v.uint.between_exclusively<0, 100>; + + public arrayField!: v.uint[]; + public optionalArrayField?: v.uint[]; + + public maxArrayField!: v.uint.max<100>[]; + +} \ No newline at end of file diff --git a/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/ValueObject/BookCopyId.ts b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/ValueObject/BookCopyId.ts index 71f443f..6c11a07 100644 --- a/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/ValueObject/BookCopyId.ts +++ b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/ValueObject/BookCopyId.ts @@ -1,3 +1,3 @@ -import { UIntValue } from "@hexancore/common"; +import { UInt } from "@hexancore/common"; -export class BookCopyId extends UIntValue { } +export class BookCopyId extends UInt { } diff --git a/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/ValueObject/BookId.ts b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/ValueObject/BookId.ts index 07bdcb2..433f33a 100644 --- a/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/ValueObject/BookId.ts +++ b/test/helper/libs/test-lib/src/Book/Domain/Book/Shared/ValueObject/BookId.ts @@ -1,3 +1,3 @@ -import { UIntValue } from "@hexancore/common"; +import { UInt } from "@hexancore/common"; -export class BookId extends UIntValue { } \ No newline at end of file +export class BookId extends UInt { } \ No newline at end of file diff --git a/test/integration/Compiler/Transformer/HObject/ArrayItemsRulesHObjectAOT.test.ts b/test/integration/Compiler/Transformer/HObject/ArrayItemsRulesHObjectAOT.test.ts new file mode 100644 index 0000000..bfac325 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/ArrayItemsRulesHObjectAOT.test.ts @@ -0,0 +1,55 @@ +/** + * @group integration + */ +import { NonMethodProperties } from "@hexancore/common"; +import { FeatureTsTransformerTestHelper } from "@test/FeatureTsTransformerTestHelper"; +import { ArrayItemsTestDto } from "@test/libs/test-lib/src/Book/Application/Book/Dto/ArrayItemsTestDto"; + +describe("HObject.Rules.ArrayItems", () => { + let helper: FeatureTsTransformerTestHelper; + const validPlain: NonMethodProperties = { + arrayMinItemsField: [1, 2, 3], + arrayMaxItemsField: [1, 2], + arrayExaclyItemsField: [1, 2], + arrayBetweenItemsField: [1], + optionalArrayItemsField: [1, 2, 3] + }; + + beforeAll(() => { + helper = FeatureTsTransformerTestHelper.create(); + }); + + test("transform", () => { + const sourceFilePath = `/Book/Application/Book/Dto/ArrayItemsTestDto.ts`; + const out = helper.transform(sourceFilePath); + + expect(out).toMatchSnapshot(); + }); + + describe("parse", () => { + test("when valid", () => { + const current = ArrayItemsTestDto.parse(validPlain); + + expect(current).toMatchSnapshot(); + }); + test("when invalid", () => { + const current = ArrayItemsTestDto.parse({ + arrayMinItemsField: [], + arrayMaxItemsField: [1,2,3], + arrayExaclyItemsField: [], + arrayBetweenItemsField: [], + optionalArrayItemsField: [] + }); + + expect(current).toMatchSnapshot(); + }); + }); + + test("toJSON", () => { + const current = ArrayItemsTestDto.cs(validPlain); + + expect(JSON.stringify(current, undefined, 2)).toMatchSnapshot(); + }); +}); + + diff --git a/test/integration/Compiler/Transformer/HObject/HCommandAOT.test.ts b/test/integration/Compiler/Transformer/HObject/HCommandAOT.test.ts new file mode 100644 index 0000000..a5a8c17 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/HCommandAOT.test.ts @@ -0,0 +1,42 @@ +/** + * @group integration + */ + +import { FeatureTsTransformerTestHelper } from "@test/FeatureTsTransformerTestHelper"; +import { BookCreateCommand } from "@test/libs/test-lib/src"; + +describe("HObject.HCommand", () => { + let helper: FeatureTsTransformerTestHelper; + + beforeAll(() => { + helper = FeatureTsTransformerTestHelper.create(); + }); + + test("transform", () => { + const sourceFilePath = `/Book/Application/Book/Command/Create/BookCreateCommand.ts`; + const out = helper.transform(sourceFilePath); + + expect(out).toMatchSnapshot(); + }); + + describe("parse", () => { + test("when valid", () => { + const current = BookCreateCommand.parse({ title: "test" }); + + expect(current).toMatchSnapshot(); + }); + test("when invalid", () => { + const current = BookCreateCommand.parse("invalid"); + + expect(current).toMatchSnapshot(); + }); + }); + + test("toJSON", () => { + const current = BookCreateCommand.cs({ title: "test" }); + + expect(JSON.stringify(current, undefined, 2)).toMatchSnapshot(); + }); +}); + + diff --git a/test/integration/Compiler/Transformer/HObject/HObjectPropertyExtractor.test.ts b/test/integration/Compiler/Transformer/HObject/HObjectPropertyExtractor.test.ts deleted file mode 100644 index 94fd38e..0000000 --- a/test/integration/Compiler/Transformer/HObject/HObjectPropertyExtractor.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @group integration - */ - -import { TsTransformerTestHelper } from "@/Compiler/Test/TsTransformerTestHelper"; -import ts from "typescript"; -import { TsTransfromerHelper } from "@/Compiler/Transformer/TsTransformerHelper"; -import { LIBS_DIRNAME } from "@test/libs"; -import type { HObjectPropertyTsMeta } from "@/Compiler/Transformer/Feature/HObject/HObjectPropertyTsMeta"; -import { HObjectPropertyExtractor } from "@/Compiler/Transformer/Feature/HObject/HObjectPropertyExtractor"; - -describe(HObjectPropertyExtractor.constructor.name, () => { - const helper = TsTransformerTestHelper.createFromTsConfig(process.cwd() + "/tsconfig.json"); - let extractor: HObjectPropertyExtractor; - - beforeAll(() => { - extractor = new HObjectPropertyExtractor(); - }); - - test("extract", () => { - const sourceFilePath = LIBS_DIRNAME + "/test-lib/src/Book/Application/Book/Dto/TestTransformDto.ts"; - const diagnostics: ts.Diagnostic[] = []; - let properties!: HObjectPropertyTsMeta[]; - const transformer = (source) => { - ts.visitNodes(source.statements, (node) => { - if (ts.isClassDeclaration(node)) { - properties = extractor.extract(node,source, diagnostics); - } - return node; - }); - - return source; - }; - - helper.transpileModule(transformer, sourceFilePath); - - expect(properties.map(p => p.toString())).toMatchSnapshot(); - expect(TsTransfromerHelper.reportDiagnostics(diagnostics, true)).toEqual(''); - }); - -}); - - diff --git a/test/integration/Compiler/Transformer/HObject/HObjectTsTransformer.test.ts b/test/integration/Compiler/Transformer/HObject/HObjectTsTransformer.test.ts deleted file mode 100644 index fab5898..0000000 --- a/test/integration/Compiler/Transformer/HObject/HObjectTsTransformer.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @group integration - */ - -import { TsTransformerTestHelper } from "@/Compiler/Test/TsTransformerTestHelper"; -import { FeatureTsTransformer } from "@/Compiler/Transformer/Feature/FeatureTsTransformer"; -import { HObjectTsTransformer } from "@/Compiler/Transformer/Feature/HObject/HObjectTsTransformer"; -import { LIBS_DIRNAME } from "@test/libs"; -import type { TransformerFactory, SourceFile } from "typescript"; - -describe(HObjectTsTransformer.constructor.name, () => { - const helper = TsTransformerTestHelper.createFromTsConfig(process.cwd() + "/tsconfig.json"); - const sourceRoot = LIBS_DIRNAME + "/test-lib/src"; - let transformer: FeatureTsTransformer; - - let transformerFactory: TransformerFactory; - - beforeAll(() => { - - const compilerRoot = process.cwd() + "/lib/Compiler"; - transformer = FeatureTsTransformer.create(sourceRoot, compilerRoot); - transformerFactory = helper.createTransformerFactory(transformer.transform.bind(transformer)); - }); - - test("transform: Command", () => { - const sourceFilePath = `${sourceRoot}/Book/Application/Book/Command/Create/BookCreateCommand.ts`; - - const out = helper.transformExistingAndReturnAsString(sourceFilePath, [transformerFactory]); - - expect(out).toMatchSnapshot(); - }); - - test("transform: Query", () => { - const sourceFilePath = `${sourceRoot}/Book/Application/Book/Query/GetById/BookGetByIdQuery.ts`; - - const out = helper.transformExistingAndReturnAsString(sourceFilePath, [transformerFactory]); - - expect(out).toMatchSnapshot(); - }); - - test("transform: DTO", () => { - const sourceFilePath = `${sourceRoot}/Book/Application/Book/Dto/TestTransformDto.ts`; - - const out = helper.transformExistingAndReturnAsString(sourceFilePath, [transformerFactory]); - - expect(out).toMatchSnapshot(); - }); - - test("transform: ValueObject", () => { - const sourceFilePath = `${sourceRoot}/Book/Domain/Book/Shared/ValueObject/BookId.ts`; - - const out = helper.transformExistingAndReturnAsString(sourceFilePath, [transformerFactory]); - - expect(out).toMatchSnapshot(); - }); - -}); - - diff --git a/test/integration/Compiler/Transformer/HObject/HQueryAOT.test.ts b/test/integration/Compiler/Transformer/HObject/HQueryAOT.test.ts new file mode 100644 index 0000000..350e954 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/HQueryAOT.test.ts @@ -0,0 +1,42 @@ +/** + * @group integration + */ + +import { FeatureTsTransformerTestHelper } from "@test/FeatureTsTransformerTestHelper"; +import { BookGetByIdQuery } from "@test/libs/test-lib/src"; + +describe("HObject.HQuery", () => { + let helper: FeatureTsTransformerTestHelper; + + beforeAll(() => { + helper = FeatureTsTransformerTestHelper.create(); + }); + + test("transform", () => { + const sourceFilePath = `/Book/Application/Book/Query/GetById/BookGetByIdQuery.ts`; + const out = helper.transform(sourceFilePath); + + expect(out).toMatchSnapshot(); + }); + + describe("parse", () => { + test("when valid", () => { + const current = BookGetByIdQuery.parse({ title: "test" }); + + expect(current).toMatchSnapshot(); + }); + test("when invalid", () => { + const current = BookGetByIdQuery.parse("invalid"); + + expect(current).toMatchSnapshot(); + }); + }); + + test("toJSON", () => { + const current = BookGetByIdQuery.cs({ title: "test" }); + + expect(JSON.stringify(current, undefined, 2)).toMatchSnapshot(); + }); +}); + + diff --git a/test/integration/Compiler/Transformer/HObject/IntRulesHObjectAOT.test.ts b/test/integration/Compiler/Transformer/HObject/IntRulesHObjectAOT.test.ts new file mode 100644 index 0000000..cb71968 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/IntRulesHObjectAOT.test.ts @@ -0,0 +1,60 @@ +/** + * @group integration + */ + +import { FeatureTsTransformerTestHelper } from "@test/FeatureTsTransformerTestHelper"; +import { IntTestDto } from "@test/libs/test-lib/src/Book/Application/Book/Dto/IntTestDto"; + +describe("HObject.Rules.Int", () => { + let helper: FeatureTsTransformerTestHelper; + const validPlain = { + field: -10, + optionalField: -20, + + minField: -100, + maxField: 100, + + betweenField: -50, + + ltField: -100, + gtField: -99, + betweenExclusivelyField: -50, + + arrayField: [-1, -2, -3], + optionalArrayField: [-10, -20, -30], + + maxArrayField: [-1, -2, -3], + }; + + beforeAll(() => { + helper = FeatureTsTransformerTestHelper.create(); + }); + + test("transform", () => { + const sourceFilePath = `/Book/Application/Book/Dto/IntTestDto.ts`; + const out = helper.transform(sourceFilePath); + + expect(out).toMatchSnapshot(); + }); + + describe("parse", () => { + test("when valid", () => { + const current = IntTestDto.parse(validPlain); + + expect(current).toMatchSnapshot(); + }); + test("when invalid", () => { + const current = IntTestDto.parse({optionalField: "invalid", optionalArrayField: "invalid"}); + + expect(current).toMatchSnapshot(); + }); + }); + + test("toJSON", () => { + const current = IntTestDto.cs(validPlain); + + expect(JSON.stringify(current, undefined, 2)).toMatchSnapshot(); + }); +}); + + diff --git a/test/integration/Compiler/Transformer/HObject/SimpleValueObjectAOT.test.ts b/test/integration/Compiler/Transformer/HObject/SimpleValueObjectAOT.test.ts new file mode 100644 index 0000000..29664bd --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/SimpleValueObjectAOT.test.ts @@ -0,0 +1,42 @@ +/** + * @group integration + */ + +import { FeatureTsTransformerTestHelper } from "@test/FeatureTsTransformerTestHelper"; +import { BookId } from "@test/libs/test-lib/src"; + +describe("HObject.SimpleValueObject", () => { + let helper: FeatureTsTransformerTestHelper; + + beforeAll(() => { + helper = FeatureTsTransformerTestHelper.create(); + }); + + test("transform", () => { + const sourceFilePath = `/Book/Domain/Book/Shared/ValueObject/BookId.ts`; + const out = helper.transform(sourceFilePath); + + expect(out).toMatchSnapshot(); + }); + + describe("parse", () => { + test("when valid", () => { + const current = BookId.parse(10); + + expect(current).toMatchSnapshot(); + }); + test("when invalid", () => { + const current = BookId.parse("invalid"); + + expect(current).toMatchSnapshot(); + }); + }); + + test("toJSON", () => { + const current = BookId.cs(10); + + expect(JSON.stringify(current, undefined, 2)).toMatchSnapshot(); + }); +}); + + diff --git a/test/integration/Compiler/Transformer/HObject/StringRulesHObjectAOT.test.ts b/test/integration/Compiler/Transformer/HObject/StringRulesHObjectAOT.test.ts new file mode 100644 index 0000000..cd3d9dd --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/StringRulesHObjectAOT.test.ts @@ -0,0 +1,70 @@ +/** + * @group integration + */ + +import { type JsonObjectType } from "@hexancore/common"; +import { FeatureTsTransformerTestHelper } from "@test/FeatureTsTransformerTestHelper"; +import { StringTestDto } from "@test/libs/test-lib/src/Book/Application/Book/Dto/StringTestDto"; + +describe("HObject.Rules.String", () => { + let helper: FeatureTsTransformerTestHelper; + const validPlain: JsonObjectType = { + field: "test", + optionalField: "test_optional", + + lengthField: "test_", + lengthMinField: "test_min", + lengthMaxField: "test_max", + + lengthBetweenField: "test_between", + + regexField: "ab123", + + arrayField: ["test_0", "test_1"], + optionalArrayField: ["test_optional_0", "test_optional_1"], + }; + + beforeAll(() => { + helper = FeatureTsTransformerTestHelper.create(); + }); + + test("transform", () => { + const sourceFilePath = `/Book/Application/Book/Dto/StringTestDto.ts`; + const out = helper.transform(sourceFilePath); + + expect(out).toMatchSnapshot(); + }); + + describe("parse", () => { + test("when valid", () => { + const current = StringTestDto.parse(validPlain); + + expect(current).toMatchSnapshot(); + }); + test("when invalid", () => { + const current = StringTestDto.parse({ + field: 100, + optionalField: 101, + + lengthField: "test_invalid", + lengthMinField: "t", + lengthMaxField: "test_max_invalid_1111", + + lengthBetweenField: "test_between__invalid", + + regexField: "a", + optionalArrayField: "invalid" + }); + + expect(current).toMatchSnapshot(); + }); + }); + + test("toJSON", () => { + const current = StringTestDto.cs(validPlain); + + expect(JSON.stringify(current, undefined, 2)).toMatchSnapshot(); + }); +}); + + diff --git a/test/integration/Compiler/Transformer/HObject/UIntRulesHObjectAOT.test.ts b/test/integration/Compiler/Transformer/HObject/UIntRulesHObjectAOT.test.ts new file mode 100644 index 0000000..6767429 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/UIntRulesHObjectAOT.test.ts @@ -0,0 +1,60 @@ +/** + * @group integration + */ + +import { FeatureTsTransformerTestHelper } from "@test/FeatureTsTransformerTestHelper"; +import { UIntTestDto } from "@test/libs/test-lib/src/Book/Application/Book/Dto/UIntTestDto"; + +describe("HObject.Rules.UInt", () => { + let helper: FeatureTsTransformerTestHelper; + const validPlain = { + field: 10, + optionalField: 20, + + minField: 0, + maxField: 100, + + betweenField: 50, + + ltField: 100, + gtField: 99, + betweenExclusivelyField: 50, + + arrayField: [1, 2, 3], + optionalArrayField: [10, 20, 30], + + maxArrayField: [1, 2, 3], + }; + + beforeAll(() => { + helper = FeatureTsTransformerTestHelper.create(); + }); + + test("transform", () => { + const sourceFilePath = `/Book/Application/Book/Dto/UIntTestDto.ts`; + const out = helper.transform(sourceFilePath); + + expect(out).toMatchSnapshot(); + }); + + describe("parse", () => { + test("when valid", () => { + const current = UIntTestDto.parse(validPlain); + + expect(current).toMatchSnapshot(); + }); + test("when invalid", () => { + const current = UIntTestDto.parse({optionalField: "invalid", optionalArrayField: "invalid"}); + + expect(current).toMatchSnapshot(); + }); + }); + + test("toJSON", () => { + const current = UIntTestDto.cs(validPlain); + + expect(JSON.stringify(current, undefined, 2)).toMatchSnapshot(); + }); +}); + + diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/ArrayItemsRulesHObjectAOT.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/ArrayItemsRulesHObjectAOT.test.ts.snap new file mode 100644 index 0000000..e3d9383 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/__snapshots__/ArrayItemsRulesHObjectAOT.test.ts.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HObject.Rules.ArrayItems parse when invalid 1`] = ` +Result { + "value": { + "cause": null, + "code": 400, + "data": { + "code": "invalid_hobject", + "i18n": "core.plain.parse_issue.invalid_hobject", + "issues": [ + { + "code": "too_small", + "current": 0, + "i18n": "core.plain.parse_issue.too_small.array_size_inclusively", + "message": "Array must contain at least 2 element(s), current: 0", + "min": 2, + "mode": "array_size_inclusively", + "path": "arrayMinItemsField", + }, + { + "code": "too_big", + "current": 3, + "i18n": "core.plain.parse_issue.too_big", + "max": 2, + "message": "Array must contain maximum 2 element(s), current: 3", + "mode": "array_size_inclusively", + "path": "arrayMaxItemsField", + }, + { + "code": "too_small", + "current": 0, + "i18n": "core.plain.parse_issue.too_small.array_size_exactly", + "message": "Array must contain exactly 2 element(s), current: 0", + "min": 2, + "mode": "array_size_exactly", + "path": "arrayExaclyItemsField", + }, + { + "code": "too_small", + "current": 0, + "i18n": "core.plain.parse_issue.too_small.array_size_inclusively", + "message": "Array must contain at least 2 element(s), current: 0", + "min": 2, + "mode": "array_size_inclusively", + "path": "optionalArrayItemsField", + }, + ], + "message": "Invalid plain object to parse to HObject: book.application.book.dto.array_items_test_dto", + "path": undefined, + "typeId": "book.application.book.dto.array_items_test_dto", + }, + "error": null, + "i18n": "", + "message": "", + "type": "core.plain.parse", + }, +} +`; + +exports[`HObject.Rules.ArrayItems parse when valid 1`] = ` +Result { + "value": { + "arrayBetweenItemsField": [ + 1, + ], + "arrayExaclyItemsField": [ + 1, + 2, + ], + "arrayMaxItemsField": [ + 1, + 2, + ], + "arrayMinItemsField": [ + 1, + 2, + 3, + ], + "optionalArrayItemsField": [ + 1, + 2, + 3, + ], + }, +} +`; + +exports[`HObject.Rules.ArrayItems toJSON 1`] = ` +"{ + "arrayMinItemsField": [ + 1, + 2, + 3 + ], + "arrayMaxItemsField": [ + 1, + 2 + ], + "arrayExaclyItemsField": [ + 1, + 2 + ], + "arrayBetweenItemsField": [ + 1 + ], + "optionalArrayItemsField": [ + 1, + 2, + 3 + ] +}" +`; + +exports[`HObject.Rules.ArrayItems transform 1`] = ` +"import { Dto, v, R, OK, PlainParseHelper, IntegerPlainParseHelper, ArrayPlainParseHelper, StringPlainParseHelper, NumberPlainParseHelper, HObjectTypeMeta, type JsonObjectType, type PlainParseError, type PlainParseIssue, type AnyDto, type DtoType } from '@hexancore/common'; +export class ArrayItemsTestDto extends Dto { + public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Dto", "ArrayItemsTestDto", ArrayItemsTestDto); + public arrayMinItemsField!: v.int[] & v.items.min<2>; + public arrayMaxItemsField!: v.int[] & v.items.max<2>; + public arrayExaclyItemsField!: v.int[] & v.items.exactly<2>; + public arrayBetweenItemsField!: v.int[] & v.items.between<0, 2>; + public optionalArrayItemsField?: v.int[] & v.items.min<2>; + public constructor(arrayMinItemsField: any, arrayMaxItemsField: any, arrayExaclyItemsField: any, arrayBetweenItemsField: any, optionalArrayItemsField?: any) { + super(); + this.arrayMinItemsField = arrayMinItemsField; + this.arrayMaxItemsField = arrayMaxItemsField; + this.arrayExaclyItemsField = arrayExaclyItemsField; + this.arrayBetweenItemsField = arrayBetweenItemsField; + this.optionalArrayItemsField = optionalArrayItemsField; + } + public static parse(this: HDtoType, plain: unknown): R { + if (typeof plain !== "object") { + return PlainParseHelper.HObjectIsNotObjectParseErr(ArrayItemsTestDto as any, plain); + } + const p = plain as Record; + const issues: PlainParseIssue[] = []; + const arrayMinItemsField = ArrayPlainParseHelper.parsePrimitiveArrayItemsMin(p.arrayMinItemsField, 2, pi => IntegerPlainParseHelper.parseInt(pi), "arrayMinItemsField", issues); + const arrayMaxItemsField = ArrayPlainParseHelper.parsePrimitiveArrayItemsMax(p.arrayMaxItemsField, 2, pi => IntegerPlainParseHelper.parseInt(pi), "arrayMaxItemsField", issues); + const arrayExaclyItemsField = ArrayPlainParseHelper.parsePrimitiveArrayItemsExactly(p.arrayExaclyItemsField, 2, pi => IntegerPlainParseHelper.parseInt(pi), "arrayExaclyItemsField", issues); + const arrayBetweenItemsField = ArrayPlainParseHelper.parsePrimitiveArrayItemsBetween(p.arrayBetweenItemsField, 0, 2, pi => IntegerPlainParseHelper.parseInt(pi), "arrayBetweenItemsField", issues); + let optionalArrayItemsField; + if (p.optionalArrayItemsField !== undefined) { + optionalArrayItemsField = ArrayPlainParseHelper.parsePrimitiveArrayItemsMin(p.optionalArrayItemsField, 2, pi => IntegerPlainParseHelper.parseInt(pi), "optionalArrayItemsField", issues); + } + if (issues.length > 0) { + return PlainParseHelper.HObjectParseErr(ArrayItemsTestDto as any, issues); + } + return OK(new ArrayItemsTestDto(arrayMinItemsField as any, arrayMaxItemsField as any, arrayExaclyItemsField as any, arrayBetweenItemsField as any, optionalArrayItemsField as any)) as any; + } + public toJSON(): JsonObjectType { + return { + arrayMinItemsField: this.arrayMinItemsField, + arrayMaxItemsField: this.arrayMaxItemsField, + arrayExaclyItemsField: this.arrayExaclyItemsField, + arrayBetweenItemsField: this.arrayBetweenItemsField, + optionalArrayItemsField: this.optionalArrayItemsField + }; + } +} +" +`; diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/HCommandAOT.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/HCommandAOT.test.ts.snap new file mode 100644 index 0000000..c792f80 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/__snapshots__/HCommandAOT.test.ts.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HObject.HCommand parse when invalid 1`] = ` +Result { + "value": { + "cause": null, + "code": 400, + "data": { + "code": "invalid_hobject", + "i18n": "core.plain.parse_issue.invalid_hobject", + "issues": [ + { + "code": "invalid_type", + "expected": "object", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'object', received: 'string'", + "path": undefined, + "received": "string", + }, + ], + "message": "Invalid plain object to parse to HObject: book.application.book.command.create", + "path": undefined, + "typeId": "book.application.book.command.create", + }, + "error": null, + "i18n": "", + "message": "", + "type": "core.plain.parse", + }, +} +`; + +exports[`HObject.HCommand parse when valid 1`] = ` +Result { + "value": { + "title": "test", + }, +} +`; + +exports[`HObject.HCommand toJSON 1`] = ` +"{ + "title": "test" +}" +`; + +exports[`HObject.HCommand transform 1`] = ` +"import { HCommand, R, OK, PlainParseHelper, IntegerPlainParseHelper, ArrayPlainParseHelper, StringPlainParseHelper, NumberPlainParseHelper, HObjectTypeMeta, type JsonObjectType, type PlainParseError, type PlainParseIssue, type AnyCommand, type CommandType } from "@hexancore/common"; +export class BookCreateCommand extends HCommand { + public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Command", "Create", BookCreateCommand); + public title!: string; + public constructor(title: any) { + super(); + this.title = title; + } + public static parse(this: HCommandType, plain: unknown): R { + if (typeof plain !== "object") { + return PlainParseHelper.HObjectIsNotObjectParseErr(BookCreateCommand as any, plain); + } + const p = plain as Record; + const issues: PlainParseIssue[] = []; + const title = StringPlainParseHelper.parseString(p.title, "title", issues); + if (issues.length > 0) { + return PlainParseHelper.HObjectParseErr(BookCreateCommand as any, issues); + } + return OK(new BookCreateCommand(title as any)) as any; + } + public toJSON(): JsonObjectType { + return { + title: this.title + }; + } +} +" +`; diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/HObjectPropertyExtractor.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/HObjectPropertyExtractor.test.ts.snap deleted file mode 100644 index c9d194f..0000000 --- a/test/integration/Compiler/Transformer/HObject/__snapshots__/HObjectPropertyExtractor.test.ts.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Function extract 1`] = ` -[ - "optionalField?: string", - "numberField!: number", - "stringField!: string", - "booleanField!: boolean", - "bigintField!: bigint", - "primitiveArrayField!: string[]", - "uintField!: v.uint", - "ruleWithArgsField!: v.int.between<-10,100>", - "ruleArrayField!: v.int.between<-10,100>[]", - "ruleArrayWithItemsField!: v.int.between<-10,100>[] & v.items.between<2,5>", - "hObjField!: RefId", - "hObjArrayField!: RefId[]", -] -`; diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/HObjectTsTransformer.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/HObjectTsTransformer.test.ts.snap deleted file mode 100644 index 062351b..0000000 --- a/test/integration/Compiler/Transformer/HObject/__snapshots__/HObjectTsTransformer.test.ts.snap +++ /dev/null @@ -1,123 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Function transform: Command 1`] = ` -"import { HCommand, R, OK, type JsonObjectType, type PlainParsableHObjectType, type PlainParseError, PlainParseHelper, InvalidTypePlainParseIssue, HObjectTypeMeta, PlainParseIssue, TooBigPlainParseIssue, TooSmallPlainParseIssue } from "@hexancore/common"; -export class BookCreateCommand extends HCommand { - public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Command", "Create", BookCreateCommand); - public title!: string; - public constructor(title: any) { - this.title = title; - } - public static parse(plain: unknown): common_1.R { - if (typeof plain !== "object") { - return PlainParseHelper.HObjectIsNotObjectParseErr(BookCreateCommand as any, plain); - } - const p = plain as Record; - const issues: PlainParseIssue[] = []; - const title = PlainParseHelper.parseString(p.title, "title", issues); - } - public static toJSON(): common_1.JsonObjectType { - return { - title: this.title - }; - } -} -" -`; - -exports[`Function transform: DTO 1`] = ` -"import { Dto, v, RefId, R, OK, type JsonObjectType, type PlainParsableHObjectType, type PlainParseError, PlainParseHelper, InvalidTypePlainParseIssue, HObjectTypeMeta, PlainParseIssue, TooBigPlainParseIssue, TooSmallPlainParseIssue } from '@hexancore/common'; -export class TestTransformDto extends Dto { - public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Dto", "TestTransformDto", TestTransformDto); - public optionalField?: string; - public numberField!: number; - public stringField!: string; - public booleanField!: boolean; - public bigintField!: bigint; - public primitiveArrayField!: string[]; - public uintField!: v.uint; - public ruleWithArgsField!: v.int.between<-10, 100>; - public ruleArrayField!: v.int.between<-10, 100>[]; - public ruleArrayWithItemsField!: v.int.between<-10, 100>[] & v.items.between<2, 5>; - public hObjField!: RefId; - public hObjArrayField!: RefId[]; - public constructor(numberField: any, stringField: any, booleanField: any, bigintField: any, primitiveArrayField: any, uintField: any, ruleWithArgsField: any, ruleArrayField: any, ruleArrayWithItemsField: any, hObjField: any, hObjArrayField: any, optionalField?: any) { - this.numberField = numberField; - this.stringField = stringField; - this.booleanField = booleanField; - this.bigintField = bigintField; - this.primitiveArrayField = primitiveArrayField; - this.uintField = uintField; - this.ruleWithArgsField = ruleWithArgsField; - this.ruleArrayField = ruleArrayField; - this.ruleArrayWithItemsField = ruleArrayWithItemsField; - this.hObjField = hObjField; - this.hObjArrayField = hObjArrayField; - this.optionalField = optionalField; - } - public static parse(plain: unknown): common_1.R { - if (typeof plain !== "object") { - return PlainParseHelper.HObjectIsNotObjectParseErr(TestTransformDto as any, plain); - } - const p = plain as Record; - const issues: PlainParseIssue[] = []; - let optionalField; - if (p.optionalField !== undefined) { - optionalField = PlainParseHelper.parseString(p.optionalField, "optionalField", issues); - } - const stringField = PlainParseHelper.parseString(p.stringField, "stringField", issues); - const primitiveArrayField = PlainParseHelper.parsePrimitiveArray(p.primitiveArrayField, PlainParseHelper.parseString, "primitiveArrayField", issues); - } - public static toJSON(): common_1.JsonObjectType { - return { - optionalField: this.optionalField, - numberField: this.numberField, - stringField: this.stringField, - booleanField: this.booleanField, - bigintField: this.bigintField.toString(), - primitiveArrayField: this.primitiveArrayField, - uintField: this.uintField, - ruleWithArgsField: this.ruleWithArgsField, - ruleArrayField: this.ruleArrayField, - ruleArrayWithItemsField: this.ruleArrayWithItemsField, - hObjField: this.hObjField, - hObjArrayField: this.hObjArrayField.map(item => item.toJSON()) - }; - } -} -" -`; - -exports[`Function transform: Query 1`] = ` -"import { HQuery, R, OK, type JsonObjectType, type PlainParsableHObjectType, type PlainParseError, PlainParseHelper, InvalidTypePlainParseIssue, HObjectTypeMeta, PlainParseIssue, TooBigPlainParseIssue, TooSmallPlainParseIssue } from "@hexancore/common"; -import type { BookDto } from "../../Dto/BookDto"; -export class BookGetByIdQuery extends HQuery { - public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Query", "GetById", BookGetByIdQuery); - public title!: string; - public constructor(title: any) { - this.title = title; - } - public static parse(plain: unknown): common_1.R { - if (typeof plain !== "object") { - return PlainParseHelper.HObjectIsNotObjectParseErr(BookGetByIdQuery as any, plain); - } - const p = plain as Record; - const issues: PlainParseIssue[] = []; - const title = PlainParseHelper.parseString(p.title, "title", issues); - } - public static toJSON(): common_1.JsonObjectType { - return { - title: this.title - }; - } -} -" -`; - -exports[`Function transform: ValueObject 1`] = ` -"import { UIntValue, R, OK, type JsonObjectType, type PlainParsableHObjectType, type PlainParseError, PlainParseHelper, InvalidTypePlainParseIssue, HObjectTypeMeta, PlainParseIssue, TooBigPlainParseIssue, TooSmallPlainParseIssue } from "@hexancore/common"; -export class BookId extends UIntValue { - public static HOBJ_META = HObjectTypeMeta.domain("Book", "Book", "ValueObject", "BookId", BookId); -} -" -`; diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/HQueryAOT.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/HQueryAOT.test.ts.snap new file mode 100644 index 0000000..a71f28e --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/__snapshots__/HQueryAOT.test.ts.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HObject.HQuery parse when invalid 1`] = ` +Result { + "value": { + "cause": null, + "code": 400, + "data": { + "code": "invalid_hobject", + "i18n": "core.plain.parse_issue.invalid_hobject", + "issues": [ + { + "code": "invalid_type", + "expected": "object", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'object', received: 'string'", + "path": undefined, + "received": "string", + }, + ], + "message": "Invalid plain object to parse to HObject: book.application.book.query.get_by_id", + "path": undefined, + "typeId": "book.application.book.query.get_by_id", + }, + "error": null, + "i18n": "", + "message": "", + "type": "core.plain.parse", + }, +} +`; + +exports[`HObject.HQuery parse when valid 1`] = ` +Result { + "value": { + "title": "test", + }, +} +`; + +exports[`HObject.HQuery toJSON 1`] = ` +"{ + "title": "test" +}" +`; + +exports[`HObject.HQuery transform 1`] = ` +"import { HQuery, R, OK, PlainParseHelper, IntegerPlainParseHelper, ArrayPlainParseHelper, StringPlainParseHelper, NumberPlainParseHelper, HObjectTypeMeta, type JsonObjectType, type PlainParseError, type PlainParseIssue, type AnyQuery, type QueryType } from "@hexancore/common"; +import type { BookDto } from "../../Dto/BookDto"; +export class BookGetByIdQuery extends HQuery { + public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Query", "GetById", BookGetByIdQuery); + public title!: string; + public constructor(title: any) { + super(); + this.title = title; + } + public static parse(this: HQueryType, plain: unknown): R { + if (typeof plain !== "object") { + return PlainParseHelper.HObjectIsNotObjectParseErr(BookGetByIdQuery as any, plain); + } + const p = plain as Record; + const issues: PlainParseIssue[] = []; + const title = StringPlainParseHelper.parseString(p.title, "title", issues); + if (issues.length > 0) { + return PlainParseHelper.HObjectParseErr(BookGetByIdQuery as any, issues); + } + return OK(new BookGetByIdQuery(title as any)) as any; + } + public toJSON(): JsonObjectType { + return { + title: this.title + }; + } +} +" +`; diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/IntRulesHObjectAOT.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/IntRulesHObjectAOT.test.ts.snap new file mode 100644 index 0000000..a76a575 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/__snapshots__/IntRulesHObjectAOT.test.ts.snap @@ -0,0 +1,245 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HObject.Rules.Int parse when invalid 1`] = ` +Result { + "value": { + "cause": null, + "code": 400, + "data": { + "code": "invalid_hobject", + "i18n": "core.plain.parse_issue.invalid_hobject", + "issues": [ + { + "code": "invalid_type", + "expected": "int", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'int', received: 'undefined'", + "path": "field", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "int", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'int', received: 'undefined'", + "path": "minField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "int", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'int', received: 'undefined'", + "path": "maxField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "int", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'int', received: 'undefined'", + "path": "betweenField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "int", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'int', received: 'undefined'", + "path": "gtField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "int", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'int', received: 'undefined'", + "path": "ltField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "int", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'int', received: 'undefined'", + "path": "betweenExclusivelyField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "array", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'array', received: 'undefined'", + "path": "arrayField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "array", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'array', received: 'undefined'", + "path": "maxArrayField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "int", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'int', received: 'string'", + "path": "optionalField", + "received": "string", + }, + { + "code": "invalid_type", + "expected": "array", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'array', received: 'string'", + "path": "optionalArrayField", + "received": "string", + }, + ], + "message": "Invalid plain object to parse to HObject: book.application.book.dto.int_test_dto", + "path": undefined, + "typeId": "book.application.book.dto.int_test_dto", + }, + "error": null, + "i18n": "", + "message": "", + "type": "core.plain.parse", + }, +} +`; + +exports[`HObject.Rules.Int parse when valid 1`] = ` +Result { + "value": { + "arrayField": [ + -1, + -2, + -3, + ], + "betweenExclusivelyField": -50, + "betweenField": -50, + "field": -10, + "gtField": -99, + "ltField": -100, + "maxArrayField": [ + -1, + -2, + -3, + ], + "maxField": 100, + "minField": -100, + "optionalArrayField": [ + -10, + -20, + -30, + ], + "optionalField": -20, + }, +} +`; + +exports[`HObject.Rules.Int toJSON 1`] = ` +"{ + "field": -10, + "optionalField": -20, + "minField": -100, + "maxField": 100, + "betweenField": -50, + "gtField": -99, + "ltField": -100, + "betweenExclusivelyField": -50, + "arrayField": [ + -1, + -2, + -3 + ], + "optionalArrayField": [ + -10, + -20, + -30 + ], + "maxArrayField": [ + -1, + -2, + -3 + ] +}" +`; + +exports[`HObject.Rules.Int transform 1`] = ` +"import { Dto, v, R, OK, PlainParseHelper, IntegerPlainParseHelper, ArrayPlainParseHelper, StringPlainParseHelper, NumberPlainParseHelper, HObjectTypeMeta, type JsonObjectType, type PlainParseError, type PlainParseIssue, type AnyDto, type DtoType } from '@hexancore/common'; +export class IntTestDto extends Dto { + public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Dto", "IntTestDto", IntTestDto); + public field!: v.int; + public optionalField?: v.int; + public minField!: v.int.min<-100>; + public maxField!: v.int.max<100>; + public betweenField!: v.int.between<-100, 100>; + public gtField!: v.int.gt<-100>; + public ltField!: v.int.lt<100>; + public betweenExclusivelyField!: v.int.between_exclusively<-100, 100>; + public arrayField!: v.int[]; + public optionalArrayField?: v.int[]; + public maxArrayField!: v.int.max<100>[]; + public constructor(field: any, minField: any, maxField: any, betweenField: any, gtField: any, ltField: any, betweenExclusivelyField: any, arrayField: any, maxArrayField: any, optionalField?: any, optionalArrayField?: any) { + super(); + this.field = field; + this.minField = minField; + this.maxField = maxField; + this.betweenField = betweenField; + this.gtField = gtField; + this.ltField = ltField; + this.betweenExclusivelyField = betweenExclusivelyField; + this.arrayField = arrayField; + this.maxArrayField = maxArrayField; + this.optionalField = optionalField; + this.optionalArrayField = optionalArrayField; + } + public static parse(this: HDtoType, plain: unknown): R { + if (typeof plain !== "object") { + return PlainParseHelper.HObjectIsNotObjectParseErr(IntTestDto as any, plain); + } + const p = plain as Record; + const issues: PlainParseIssue[] = []; + const field = IntegerPlainParseHelper.parseInt(p.field, "field", issues); + const minField = IntegerPlainParseHelper.parseIntGTE(p.minField, -100, "minField", issues); + const maxField = IntegerPlainParseHelper.parseIntLTE(p.maxField, 100, "maxField", issues); + const betweenField = IntegerPlainParseHelper.parseIntBetween(p.betweenField, -100, 100, "betweenField", issues); + const gtField = IntegerPlainParseHelper.parseIntGT(p.gtField, -100, "gtField", issues); + const ltField = IntegerPlainParseHelper.parseIntLT(p.ltField, 100, "ltField", issues); + const betweenExclusivelyField = IntegerPlainParseHelper.parseIntBetweenExclusively(p.betweenExclusivelyField, -100, 100, "betweenExclusivelyField", issues); + const arrayField = ArrayPlainParseHelper.parsePrimitiveArray(p.arrayField, pi => IntegerPlainParseHelper.parseInt(pi), "arrayField", issues); + const maxArrayField = ArrayPlainParseHelper.parsePrimitiveArray(p.maxArrayField, pi => IntegerPlainParseHelper.parseIntLTE(pi, 100), "maxArrayField", issues); + let optionalField; + if (p.optionalField !== undefined) { + optionalField = IntegerPlainParseHelper.parseInt(p.optionalField, "optionalField", issues); + } + let optionalArrayField; + if (p.optionalArrayField !== undefined) { + optionalArrayField = ArrayPlainParseHelper.parsePrimitiveArray(p.optionalArrayField, pi => IntegerPlainParseHelper.parseInt(pi), "optionalArrayField", issues); + } + if (issues.length > 0) { + return PlainParseHelper.HObjectParseErr(IntTestDto as any, issues); + } + return OK(new IntTestDto(field as any, minField as any, maxField as any, betweenField as any, gtField as any, ltField as any, betweenExclusivelyField as any, arrayField as any, maxArrayField as any, optionalField as any, optionalArrayField as any)) as any; + } + public toJSON(): JsonObjectType { + return { + field: this.field, + optionalField: this.optionalField, + minField: this.minField, + maxField: this.maxField, + betweenField: this.betweenField, + gtField: this.gtField, + ltField: this.ltField, + betweenExclusivelyField: this.betweenExclusivelyField, + arrayField: this.arrayField, + optionalArrayField: this.optionalArrayField, + maxArrayField: this.maxArrayField + }; + } +} +" +`; diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/SimpleValueObjectAOT.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/SimpleValueObjectAOT.test.ts.snap new file mode 100644 index 0000000..9f5eab3 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/__snapshots__/SimpleValueObjectAOT.test.ts.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HObject.SimpleValueObject parse when invalid 1`] = ` +Result { + "value": { + "cause": null, + "code": 400, + "data": { + "code": "invalid_hobject", + "i18n": "core.plain.parse_issue.invalid_hobject", + "issues": [ + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'string'", + "path": undefined, + "received": "string", + }, + ], + "message": "Invalid plain object to parse to HObject: book.domain.book.value_object.book_id", + "path": undefined, + "typeId": "book.domain.book.value_object.book_id", + }, + "error": null, + "i18n": "", + "message": "", + "type": "core.plain.parse", + }, +} +`; + +exports[`HObject.SimpleValueObject parse when valid 1`] = ` +Result { + "value": 10, +} +`; + +exports[`HObject.SimpleValueObject toJSON 1`] = `"10"`; + +exports[`HObject.SimpleValueObject transform 1`] = ` +"import { UInt, R, OK, PlainParseHelper, IntegerPlainParseHelper, ArrayPlainParseHelper, StringPlainParseHelper, NumberPlainParseHelper, HObjectTypeMeta, type JsonObjectType, type PlainParseError, type PlainParseIssue, type AnyValueObject, type ValueObjectType } from "@hexancore/common"; +export class BookId extends UInt { + public static HOBJ_META = HObjectTypeMeta.domain("Book", "Book", "ValueObject", "BookId", BookId); +} +" +`; diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/StringRulesHObjectAOT.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/StringRulesHObjectAOT.test.ts.snap new file mode 100644 index 0000000..901d2b0 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/__snapshots__/StringRulesHObjectAOT.test.ts.snap @@ -0,0 +1,213 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HObject.Rules.String parse when invalid 1`] = ` +Result { + "value": { + "cause": null, + "code": 400, + "data": { + "code": "invalid_hobject", + "i18n": "core.plain.parse_issue.invalid_hobject", + "issues": [ + { + "code": "invalid_type", + "expected": "string", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'string', received: 'number'", + "path": "field", + "received": "number", + }, + { + "code": "too_big", + "current": 12, + "i18n": "core.plain.parse_issue.too_big", + "max": 5, + "message": "String must contain exactly 5 character(s), current: 12", + "mode": "string_len_exactly", + "path": "lengthField", + }, + { + "code": "too_small", + "current": 1, + "i18n": "core.plain.parse_issue.too_small.string_len_inclusively", + "message": "String must contain at least 5 character(s), current: 1", + "min": 5, + "mode": "string_len_inclusively", + "path": "lengthMinField", + }, + { + "code": "too_big", + "current": 21, + "i18n": "core.plain.parse_issue.too_big", + "max": 20, + "message": "String must contain maximum 20 character(s), current: 21", + "mode": "string_len_inclusively", + "path": "lengthMaxField", + }, + { + "code": "out_of_range", + "current": 21, + "i18n": "core.plain.parse_issue.out_of_range.number.inclusively", + "inclusively": true, + "max": 20, + "message": "String length must be between 5 and 20 inclusively, current: 21", + "min": 5, + "path": "lengthBetweenField", + "valueType": "number", + }, + { + "code": "invalid_string", + "i18n": "core.plain.parse_issue.invalid_string.regex", + "message": "String must pass pattern: /[a-z]{2}\\d{3}/", + "path": "regexField", + "validatorArgs": { + "regex": /\\[a-z\\]\\{2\\}\\\\d\\{3\\}/, + }, + "validatorType": "regex", + }, + { + "code": "invalid_type", + "expected": "array", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'array', received: 'undefined'", + "path": "arrayField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "string", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'string', received: 'number'", + "path": "optionalField", + "received": "number", + }, + { + "code": "invalid_type", + "expected": "array", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'array', received: 'string'", + "path": "optionalArrayField", + "received": "string", + }, + ], + "message": "Invalid plain object to parse to HObject: book.application.book.dto.string_test_dto", + "path": undefined, + "typeId": "book.application.book.dto.string_test_dto", + }, + "error": null, + "i18n": "", + "message": "", + "type": "core.plain.parse", + }, +} +`; + +exports[`HObject.Rules.String parse when valid 1`] = ` +Result { + "value": { + "arrayField": [ + "test_0", + "test_1", + ], + "field": "test", + "lengthBetweenField": "test_between", + "lengthField": "test_", + "lengthMaxField": "test_max", + "lengthMinField": "test_min", + "optionalArrayField": [ + "test_optional_0", + "test_optional_1", + ], + "optionalField": "test_optional", + "regexField": "ab123", + }, +} +`; + +exports[`HObject.Rules.String toJSON 1`] = ` +"{ + "field": "test", + "optionalField": "test_optional", + "lengthField": "test_", + "lengthMinField": "test_min", + "lengthMaxField": "test_max", + "lengthBetweenField": "test_between", + "regexField": "ab123", + "arrayField": [ + "test_0", + "test_1" + ], + "optionalArrayField": [ + "test_optional_0", + "test_optional_1" + ] +}" +`; + +exports[`HObject.Rules.String transform 1`] = ` +"import { Dto, v, R, OK, PlainParseHelper, IntegerPlainParseHelper, ArrayPlainParseHelper, StringPlainParseHelper, NumberPlainParseHelper, HObjectTypeMeta, type JsonObjectType, type PlainParseError, type PlainParseIssue, type AnyDto, type DtoType } from '@hexancore/common'; +export class StringTestDto extends Dto { + public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Dto", "StringTestDto", StringTestDto); + public field!: string; + public optionalField?: string; + public lengthField!: v.string.length<5>; + public lengthMinField!: v.string.length.min<5>; + public lengthMaxField!: v.string.length.max<20>; + public lengthBetweenField!: v.string.length.between<5, 20>; + public regexField!: v.string.regex<'[a-z]{2}\\\\d{3}'>; + public arrayField!: v.string.length.min<5>[]; + public optionalArrayField?: v.string.length.min<5>[]; + public constructor(field: any, lengthField: any, lengthMinField: any, lengthMaxField: any, lengthBetweenField: any, regexField: any, arrayField: any, optionalField?: any, optionalArrayField?: any) { + super(); + this.field = field; + this.lengthField = lengthField; + this.lengthMinField = lengthMinField; + this.lengthMaxField = lengthMaxField; + this.lengthBetweenField = lengthBetweenField; + this.regexField = regexField; + this.arrayField = arrayField; + this.optionalField = optionalField; + this.optionalArrayField = optionalArrayField; + } + public static parse(this: HDtoType, plain: unknown): R { + if (typeof plain !== "object") { + return PlainParseHelper.HObjectIsNotObjectParseErr(StringTestDto as any, plain); + } + const p = plain as Record; + const issues: PlainParseIssue[] = []; + const field = StringPlainParseHelper.parseString(p.field, "field", issues); + const lengthField = StringPlainParseHelper.parseStringLength(p.lengthField, 5, "lengthField", issues); + const lengthMinField = StringPlainParseHelper.parseStringLengthMin(p.lengthMinField, 5, "lengthMinField", issues); + const lengthMaxField = StringPlainParseHelper.parseStringLengthMax(p.lengthMaxField, 20, "lengthMaxField", issues); + const lengthBetweenField = StringPlainParseHelper.parseStringLengthBetween(p.lengthBetweenField, 5, 20, "lengthBetweenField", issues); + const regexField = StringPlainParseHelper.parseStringRegex(p.regexField, /[a-z]{2}\\d{3}/, "regexField", issues); + const arrayField = ArrayPlainParseHelper.parsePrimitiveArray(p.arrayField, pi => StringPlainParseHelper.parseStringLengthMin(pi, 5), "arrayField", issues); + let optionalField; + if (p.optionalField !== undefined) { + optionalField = StringPlainParseHelper.parseString(p.optionalField, "optionalField", issues); + } + let optionalArrayField; + if (p.optionalArrayField !== undefined) { + optionalArrayField = ArrayPlainParseHelper.parsePrimitiveArray(p.optionalArrayField, pi => StringPlainParseHelper.parseStringLengthMin(pi, 5), "optionalArrayField", issues); + } + if (issues.length > 0) { + return PlainParseHelper.HObjectParseErr(StringTestDto as any, issues); + } + return OK(new StringTestDto(field as any, lengthField as any, lengthMinField as any, lengthMaxField as any, lengthBetweenField as any, regexField as any, arrayField as any, optionalField as any, optionalArrayField as any)) as any; + } + public toJSON(): JsonObjectType { + return { + field: this.field, + optionalField: this.optionalField, + lengthField: this.lengthField, + lengthMinField: this.lengthMinField, + lengthMaxField: this.lengthMaxField, + lengthBetweenField: this.lengthBetweenField, + regexField: this.regexField, + arrayField: this.arrayField, + optionalArrayField: this.optionalArrayField + }; + } +} +" +`; diff --git a/test/integration/Compiler/Transformer/HObject/__snapshots__/UIntRulesHObjectAOT.test.ts.snap b/test/integration/Compiler/Transformer/HObject/__snapshots__/UIntRulesHObjectAOT.test.ts.snap new file mode 100644 index 0000000..1d07769 --- /dev/null +++ b/test/integration/Compiler/Transformer/HObject/__snapshots__/UIntRulesHObjectAOT.test.ts.snap @@ -0,0 +1,246 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HObject.Rules.UInt parse when invalid 1`] = ` +Result { + "value": { + "cause": null, + "code": 400, + "data": { + "code": "invalid_hobject", + "i18n": "core.plain.parse_issue.invalid_hobject", + "issues": [ + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'undefined'", + "path": "field", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'undefined'", + "path": "minField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'undefined'", + "path": "maxField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'undefined'", + "path": "betweenField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'undefined'", + "path": "gtField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'undefined'", + "path": "ltField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'undefined'", + "path": "betweenExclusivelyField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "array", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'array', received: 'undefined'", + "path": "arrayField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "array", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'array', received: 'undefined'", + "path": "maxArrayField", + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "uint", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'uint', received: 'string'", + "path": "optionalField", + "received": "string", + }, + { + "code": "invalid_type", + "expected": "array", + "i18n": "core.plain.parse_issue.invalid_type", + "message": "Expected value type: 'array', received: 'string'", + "path": "optionalArrayField", + "received": "string", + }, + ], + "message": "Invalid plain object to parse to HObject: book.application.book.dto.u_int_test_dto", + "path": undefined, + "typeId": "book.application.book.dto.u_int_test_dto", + }, + "error": null, + "i18n": "", + "message": "", + "type": "core.plain.parse", + }, +} +`; + +exports[`HObject.Rules.UInt parse when valid 1`] = ` +Result { + "value": { + "cause": null, + "code": 400, + "data": { + "code": "invalid_hobject", + "i18n": "core.plain.parse_issue.invalid_hobject", + "issues": [ + { + "code": "too_big", + "current": 100, + "i18n": "core.plain.parse_issue.too_big", + "max": 100, + "message": "Number must be less than 100, current: 100", + "mode": "number_exclusively", + "path": "ltField", + }, + ], + "message": "Invalid plain object to parse to HObject: book.application.book.dto.u_int_test_dto", + "path": undefined, + "typeId": "book.application.book.dto.u_int_test_dto", + }, + "error": null, + "i18n": "", + "message": "", + "type": "core.plain.parse", + }, +} +`; + +exports[`HObject.Rules.UInt toJSON 1`] = ` +"{ + "field": 10, + "optionalField": 20, + "minField": 0, + "maxField": 100, + "betweenField": 50, + "gtField": 99, + "ltField": 100, + "betweenExclusivelyField": 50, + "arrayField": [ + 1, + 2, + 3 + ], + "optionalArrayField": [ + 10, + 20, + 30 + ], + "maxArrayField": [ + 1, + 2, + 3 + ] +}" +`; + +exports[`HObject.Rules.UInt transform 1`] = ` +"import { Dto, v, R, OK, PlainParseHelper, IntegerPlainParseHelper, ArrayPlainParseHelper, StringPlainParseHelper, NumberPlainParseHelper, HObjectTypeMeta, type JsonObjectType, type PlainParseError, type PlainParseIssue, type AnyDto, type DtoType } from '@hexancore/common'; +export class UIntTestDto extends Dto { + public static HOBJ_META = HObjectTypeMeta.application("Book", "Book", "Dto", "UIntTestDto", UIntTestDto); + public field!: v.uint; + public optionalField?: v.uint; + public minField!: v.uint.min<0>; + public maxField!: v.uint.max<100>; + public betweenField!: v.uint.between<0, 100>; + public gtField!: v.uint.gt<0>; + public ltField!: v.uint.lt<100>; + public betweenExclusivelyField!: v.uint.between_exclusively<0, 100>; + public arrayField!: v.uint[]; + public optionalArrayField?: v.uint[]; + public maxArrayField!: v.uint.max<100>[]; + public constructor(field: any, minField: any, maxField: any, betweenField: any, gtField: any, ltField: any, betweenExclusivelyField: any, arrayField: any, maxArrayField: any, optionalField?: any, optionalArrayField?: any) { + super(); + this.field = field; + this.minField = minField; + this.maxField = maxField; + this.betweenField = betweenField; + this.gtField = gtField; + this.ltField = ltField; + this.betweenExclusivelyField = betweenExclusivelyField; + this.arrayField = arrayField; + this.maxArrayField = maxArrayField; + this.optionalField = optionalField; + this.optionalArrayField = optionalArrayField; + } + public static parse(this: HDtoType, plain: unknown): R { + if (typeof plain !== "object") { + return PlainParseHelper.HObjectIsNotObjectParseErr(UIntTestDto as any, plain); + } + const p = plain as Record; + const issues: PlainParseIssue[] = []; + const field = IntegerPlainParseHelper.parseUInt(p.field, "field", issues); + const minField = IntegerPlainParseHelper.parseUIntGTE(p.minField, 0, "minField", issues); + const maxField = IntegerPlainParseHelper.parseUIntLTE(p.maxField, 100, "maxField", issues); + const betweenField = IntegerPlainParseHelper.parseUIntBetween(p.betweenField, 0, 100, "betweenField", issues); + const gtField = IntegerPlainParseHelper.parseUIntGT(p.gtField, 0, "gtField", issues); + const ltField = IntegerPlainParseHelper.parseUIntLT(p.ltField, 100, "ltField", issues); + const betweenExclusivelyField = IntegerPlainParseHelper.parseUIntBetweenExclusively(p.betweenExclusivelyField, 0, 100, "betweenExclusivelyField", issues); + const arrayField = ArrayPlainParseHelper.parsePrimitiveArray(p.arrayField, pi => IntegerPlainParseHelper.parseUInt(pi), "arrayField", issues); + const maxArrayField = ArrayPlainParseHelper.parsePrimitiveArray(p.maxArrayField, pi => IntegerPlainParseHelper.parseUIntLTE(pi, 100), "maxArrayField", issues); + let optionalField; + if (p.optionalField !== undefined) { + optionalField = IntegerPlainParseHelper.parseUInt(p.optionalField, "optionalField", issues); + } + let optionalArrayField; + if (p.optionalArrayField !== undefined) { + optionalArrayField = ArrayPlainParseHelper.parsePrimitiveArray(p.optionalArrayField, pi => IntegerPlainParseHelper.parseUInt(pi), "optionalArrayField", issues); + } + if (issues.length > 0) { + return PlainParseHelper.HObjectParseErr(UIntTestDto as any, issues); + } + return OK(new UIntTestDto(field as any, minField as any, maxField as any, betweenField as any, gtField as any, ltField as any, betweenExclusivelyField as any, arrayField as any, maxArrayField as any, optionalField as any, optionalArrayField as any)) as any; + } + public toJSON(): JsonObjectType { + return { + field: this.field, + optionalField: this.optionalField, + minField: this.minField, + maxField: this.maxField, + betweenField: this.betweenField, + gtField: this.gtField, + ltField: this.ltField, + betweenExclusivelyField: this.betweenExclusivelyField, + arrayField: this.arrayField, + optionalArrayField: this.optionalArrayField, + maxArrayField: this.maxArrayField + }; + } +} +" +`; diff --git a/test/unit/Util/Feature/__snapshots__/FeatureModuleDiscoverer.test.ts.snap b/test/unit/Util/Feature/__snapshots__/FeatureModuleDiscoverer.test.ts.snap index f8a4f04..0d1299a 100644 --- a/test/unit/Util/Feature/__snapshots__/FeatureModuleDiscoverer.test.ts.snap +++ b/test/unit/Util/Feature/__snapshots__/FeatureModuleDiscoverer.test.ts.snap @@ -2,7 +2,7 @@ exports[`Function discoverAll 1`] = ` "- name: Book - cacheKey: 76938fdd0fe1c572144becfed1ef7427b3f52342 + cacheKey: b44e0f68146acb946dab46987c547d2664ce6088 application: commands: - name: Create @@ -11,12 +11,27 @@ exports[`Function discoverAll 1`] = ` - name: GetById context: Book dtos: + - name: ArrayItemsTestDto + context: Book + path: Application/Book/Dto/ArrayItemsTestDto - name: BookDto context: Book path: Application/Book/Dto/BookDto + - name: FloatTestDto + context: Book + path: Application/Book/Dto/FloatTestDto + - name: IntTestDto + context: Book + path: Application/Book/Dto/IntTestDto + - name: StringTestDto + context: Book + path: Application/Book/Dto/StringTestDto - name: TestTransformDto context: Book path: Application/Book/Dto/TestTransformDto + - name: UIntTestDto + context: Book + path: Application/Book/Dto/UIntTestDto services: - name: BookInfraPort context: Book @@ -64,16 +79,41 @@ exports[`Function hObjectMap 1`] = ` className: BookGetByIdQuery handlerClass: BookGetByIdQueryHandler path: Application/Book/Query/GetById + Application/Book/Dto/ArrayItemsTestDto.ts: + name: ArrayItemsTestDto + context: Book + path: Application/Book/Dto/ArrayItemsTestDto + layer: application Application/Book/Dto/BookDto.ts: name: BookDto context: Book path: Application/Book/Dto/BookDto layer: application + Application/Book/Dto/FloatTestDto.ts: + name: FloatTestDto + context: Book + path: Application/Book/Dto/FloatTestDto + layer: application + Application/Book/Dto/IntTestDto.ts: + name: IntTestDto + context: Book + path: Application/Book/Dto/IntTestDto + layer: application + Application/Book/Dto/StringTestDto.ts: + name: StringTestDto + context: Book + path: Application/Book/Dto/StringTestDto + layer: application Application/Book/Dto/TestTransformDto.ts: name: TestTransformDto context: Book path: Application/Book/Dto/TestTransformDto layer: application + Application/Book/Dto/UIntTestDto.ts: + name: UIntTestDto + context: Book + path: Application/Book/Dto/UIntTestDto + layer: application Domain/Book/Shared/ValueObject/BookCopyId.ts: name: BookCopyId context: Book diff --git a/yarn.lock b/yarn.lock index d69cd2e..a4f652c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -671,9 +671,9 @@ __metadata: languageName: node linkType: hard -"@hexancore/common@npm:^0.16.0": - version: 0.16.0 - resolution: "@hexancore/common@npm:0.16.0" +"@hexancore/common@npm:0.16.1": + version: 0.16.1 + resolution: "@hexancore/common@npm:0.16.1" dependencies: "@js-joda/core": "npm:^5.6.1" class-transformer: "npm:^0.5.1" @@ -685,7 +685,7 @@ __metadata: peerDependenciesMeta: axios: optional: true - checksum: 10c0/1f80c1838bd44331ba7094874166aefaf3781ae6434f42a5f8adc40c6b841834a4fe02e51bdc58e4267bd9fd573952a9f189efd3e922d20d8060ddfa6308cff7 + checksum: 10c0/d628db6daa7573a797a0d5510ed332d28400cb1554f2e04656ad3e7b1aec87a028f13342e4cdf8e54591360f4894e2cbe5b252d644da0405228ae2f5a37481bf languageName: node linkType: hard @@ -699,7 +699,7 @@ __metadata: "@fastify/multipart": "npm:^8.3.0" "@fastify/static": "npm:^7.0.4" "@fastify/swagger": "npm:^8.14.0" - "@hexancore/common": "npm:^0.16.0" + "@hexancore/common": "npm:0.16.1" "@hexancore/mocker": "npm:^1.1.2" "@nestjs/cli": "npm:^10.3.2" "@nestjs/common": "npm:^10.3.9" @@ -742,7 +742,7 @@ __metadata: ts-node: "npm:^10.9.2" tsconfig-paths: "npm:^4.2.0" tslib: "npm:^2.6.3" - typescript: "npm:5.4.5" + typescript: "npm:5.6.2" peerDependencies: "@hexancore/common": ^0.16.0 "@nestjs/common": ^10.3.9 @@ -7494,13 +7494,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:5.4.5": - version: 5.4.5 - resolution: "typescript@npm:5.4.5" +"typescript@npm:5.6.2": + version: 5.6.2 + resolution: "typescript@npm:5.6.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e + checksum: 10c0/3ed8297a8c7c56b7fec282532503d1ac795239d06e7c4966b42d4330c6cf433a170b53bcf93a130a7f14ccc5235de5560df4f1045eb7f3550b46ebed16d3c5e5 languageName: node linkType: hard @@ -7514,13 +7514,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A5.4.5#optional!builtin": - version: 5.4.5 - resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=d69c25" +"typescript@patch:typescript@npm%3A5.6.2#optional!builtin": + version: 5.6.2 + resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin::version=5.6.2&hash=d69c25" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/9cf4c053893bcf327d101b6c024a55baf05430dc30263f9adb1bf354aeffc11306fe1f23ba2f9a0209674359f16219b5b7d229e923477b94831d07d5a33a4217 + checksum: 10c0/e6c1662e4852e22fe4bbdca471dca3e3edc74f6f1df043135c44a18a7902037023ccb0abdfb754595ca9028df8920f2f8492c00fc3cbb4309079aae8b7de71cd languageName: node linkType: hard