From 9443466ea5ad8d0fda23538109ab9d5f3284ddf4 Mon Sep 17 00:00:00 2001 From: Tom Schindl Date: Wed, 2 Oct 2024 21:48:22 +0200 Subject: [PATCH] improve code gen --- dsl/src/cli/java-rest-client-jdk/client.ts | 17 +- dsl/src/cli/java-rest-client-jdk/dto-utils.ts | 8 + dsl/src/cli/java-rest-client-jdk/generator.ts | 5 +- dsl/src/cli/java-rest-client-jdk/record.ts | 226 ++++-------------- dsl/src/cli/java-rest-client-jdk/shared.ts | 182 ++++++++++++++ dsl/src/cli/java-rest-client-jdk/union.ts | 142 +++++++++++ dsl/src/cli/model.ts | 4 + 7 files changed, 395 insertions(+), 189 deletions(-) create mode 100644 dsl/src/cli/java-rest-client-jdk/shared.ts create mode 100644 dsl/src/cli/java-rest-client-jdk/union.ts diff --git a/dsl/src/cli/java-rest-client-jdk/client.ts b/dsl/src/cli/java-rest-client-jdk/client.ts index 8bb5665..9e056e3 100644 --- a/dsl/src/cli/java-rest-client-jdk/client.ts +++ b/dsl/src/cli/java-rest-client-jdk/client.ts @@ -1,7 +1,7 @@ import { CompositeGeneratorNode, NL, toString } from "langium/generate"; import { Artifact, ArtifactGenerationConfig } from "../artifact-generator.js"; import { JavaImportsCollector, JavaRestClientJDKGeneratorConfig, generateCompilationUnit, toPath } from "../java-gen-utils.js"; -import { isMResolvedRecordType, MResolvedRSDModel } from "../model.js"; +import { isMResolvedRecordType, isMResolvedUnionType, MResolvedRSDModel } from "../model.js"; export function generateClient(m: MResolvedRSDModel, generatorConfig: ArtifactGenerationConfig, artifactConfig: JavaRestClientJDKGeneratorConfig): Artifact { const packageName = `${artifactConfig.rootPackageName}.jdkhttp`; @@ -32,9 +32,18 @@ export function generateClient(m: MResolvedRSDModel, generatorConfig: ArtifactGe .filter(isMResolvedRecordType) .filter( e => e.resolved.unions.length === 0) .forEach( e => { - const type = fqn(`${basePackage}.dto.${e.name}DTO`); - const implType = fqn(`${packageName}.impl.dto.${e.name}DTOImpl`); - staticBody.append(`registerBuilderCreator(${type}.Builder.class, ${implType}.BuilderImpl::new);`, NL) + const type = fqn(`${basePackage}.dto.${e.name}DTO`); + const implType = fqn(`${packageName}.impl.dto.${e.name}DTOImpl`); + staticBody.append(`registerBuilderCreator(${type}.Builder.class, ${implType}.BuilderImpl::new);`, NL) + } ) + m.elements + .filter(isMResolvedUnionType) + .forEach( u => { + const type = fqn(`${basePackage}.dto.${u.name}DTO`); + const implType = fqn(`${packageName}.impl.dto.${u.name}DTOImpl`); + u.resolved.records.forEach( e => { + staticBody.append(`registerBuilderCreator(${type}.${e.name}DTO.Builder.class, ${implType}.${e.name}DTOImpl.BuilderImpl::new);`, NL) + } ); } ) if( m.services.length > 0 ) { staticBody.appendNewLine(); diff --git a/dsl/src/cli/java-rest-client-jdk/dto-utils.ts b/dsl/src/cli/java-rest-client-jdk/dto-utils.ts index fe24d5f..9337d48 100644 --- a/dsl/src/cli/java-rest-client-jdk/dto-utils.ts +++ b/dsl/src/cli/java-rest-client-jdk/dto-utils.ts @@ -201,6 +201,14 @@ export function generateDTOUtils(artifactConfig: JavaRestClientJDKGeneratorConfi return List.of(); } + public static List mapObjects(JsonArray array, Function converter) { + return array + .getValuesAs(JsonObject.class) + .stream() + .map(converter) + .toList(); + } + public static List mapLiterals(JsonObject object, String property, Function mapper) { if (object.containsKey(property)) { return object.getJsonArray(property) diff --git a/dsl/src/cli/java-rest-client-jdk/generator.ts b/dsl/src/cli/java-rest-client-jdk/generator.ts index 7dcf385..7d36297 100644 --- a/dsl/src/cli/java-rest-client-jdk/generator.ts +++ b/dsl/src/cli/java-rest-client-jdk/generator.ts @@ -1,12 +1,13 @@ import chalk from "chalk"; import { Artifact, ArtifactGenerationConfig, ArtifactGeneratorConfig } from "../artifact-generator.js"; -import { isMRecordType, MResolvedRSDModel, MResolvedUserType } from "../model.js"; +import { isMRecordType, isMUnionType, MResolvedRSDModel, MResolvedUserType } from "../model.js"; import { generateClient } from "./client.js"; import { isJavaRestClientJDKGeneratorConfig, JavaRestClientJDKGeneratorConfig } from "../java-gen-utils.js"; import { generateBaseDTO } from "./base-dto.js"; import { isDefined } from "../util.js"; import { generateRecord } from "./record.js"; import { generateDTOUtils } from "./dto-utils.js"; +import { generateUnion } from "./union.js"; export function generate(model: MResolvedRSDModel, generatorConfig: ArtifactGenerationConfig, artifactConfig: ArtifactGeneratorConfig): readonly Artifact [] { console.log(chalk.cyan('Generating Java-JDK-REST-Client')); @@ -28,6 +29,8 @@ export function generate(model: MResolvedRSDModel, generatorConfig: ArtifactGene function generateType(t: MResolvedUserType, model: MResolvedRSDModel, artifactConfig: JavaRestClientJDKGeneratorConfig): Artifact | undefined { if( isMRecordType(t) ) { return generateRecord(t, model, artifactConfig); + } else if( isMUnionType(t) ) { + return generateUnion(t, artifactConfig); } return undefined; } diff --git a/dsl/src/cli/java-rest-client-jdk/record.ts b/dsl/src/cli/java-rest-client-jdk/record.ts index 06ddeb0..23ff46e 100644 --- a/dsl/src/cli/java-rest-client-jdk/record.ts +++ b/dsl/src/cli/java-rest-client-jdk/record.ts @@ -1,9 +1,26 @@ -import { CompositeGeneratorNode, IndentNode, NL, toString } from "langium/generate"; -import { Artifact } from "../artifact-generator.js"; -import { JavaImportsCollector, JavaRestClientJDKGeneratorConfig, builtinToJavaType, generateCompilationUnit, toPath } from "../java-gen-utils.js"; -import { allRecordProperties, isMBuiltinType, isMKeyProperty, isMProperty, isMRevisionProperty, isMUnionType, MBuiltinType, MKeyProperty, MProperty, MResolvedRecordType, MResolvedRSDModel, MRevisionProperty } from "../model.js"; -import { toType } from "../java-client-api/shared.js"; +import { + CompositeGeneratorNode, + IndentNode, + NL, + toString +} from "langium/generate"; +import { + Artifact +} from "../artifact-generator.js"; +import { + JavaImportsCollector, + JavaRestClientJDKGeneratorConfig, + generateCompilationUnit, + toPath +} from "../java-gen-utils.js"; +import { + allRecordProperties, + isMProperty, + isMUnionType, + MProperty, MResolvedRecordType, MResolvedRSDModel +} from "../model.js"; import { toFirstUpper } from "../util.js"; +import { generateBuilderProperty, generateProperty } from "./shared.js"; export function generateRecord(t: MResolvedRecordType, model: MResolvedRSDModel, artifactConfig: JavaRestClientJDKGeneratorConfig): Artifact | undefined { if( t.resolved.unions.length === 1 ) { @@ -16,7 +33,7 @@ export function generateRecord(t: MResolvedRecordType, model: MResolvedRSDModel, const fqn = importCollector.importType.bind(importCollector); return { - name: `${t.name}DTOImpl_.java`, + name: `${t.name}DTOImpl.java`, content: toString(generateCompilationUnit(packageName, importCollector, generateRecordContent(t, model, artifactConfig, fqn))), path: toPath(artifactConfig.targetFolder, packageName) }; @@ -32,6 +49,7 @@ export function generateRecordContent(t: MResolvedRecordType, model: MResolvedRS const DTOInterface = fqn(`${artifactConfig.rootPackageName}.dto.${t.name}DTO`) const JsonObject = fqn('jakarta.json.JsonObject') + const JsonArray = fqn('jakarta.json.JsonArray') const Json = fqn('jakarta.json.Json') const JsonObjectBuilder = fqn('jakarta.json.JsonObjectBuilder'); @@ -57,6 +75,22 @@ export function generateRecordContent(t: MResolvedRecordType, model: MResolvedRS }); classBody.append('}', NL) classBody.appendNewLine() + classBody.append(`public static ${fqn('java.util.List')}<${t.name}DTO> of(${JsonArray} data) {`, NL) + classBody.indent( methodBody => { + methodBody.append(`return DTOUtils.mapObjects(data, ${t.name}DTOImpl::of);`, NL) + }); + classBody.append('}', NL) + const keyProp = t.properties.find( e => e["@type"] === 'KeyProperty'); + if( keyProp ) { + classBody.appendNewLine(); + classBody.append('@Override',NL) + classBody.append('public String toString() {',NL) + classBody.indent( methodBody => { + methodBody.append(`return "%s[%s=%s]".formatted(getClass().getSimpleName(), "${keyProp.name}", ${keyProp.name}());`,NL) + }); + classBody.append('}',NL) + } + classBody.appendNewLine() classBody.append('public static class BuilderImpl implements Builder {', NL) classBody.indent( builderBody => { builderBody.append(`private ${JsonObjectBuilder} builder = ${Json}.createObjectBuilder();`, NL) @@ -74,13 +108,14 @@ export function generateRecordContent(t: MResolvedRecordType, model: MResolvedRS builderBody.appendNewLine(); builderBody.append(`public ${DTOInterface} build() {`, NL) builderBody.indent( methodBody => { - methodBody.append(`return new ${t.name}DTOImpl(builder.build());`); + methodBody.append(`return new ${t.name}DTOImpl(builder.build());`, NL); }); builderBody.append('}', NL) }) classBody.append('}', NL) } ) + node.append('}', NL) @@ -119,180 +154,3 @@ function generateBuilderWith(node: IndentNode, property: MProperty, model: MReso }) node.append('}', NL) } - -function generateBuilderProperty(node: IndentNode, property: MKeyProperty | MRevisionProperty | MProperty, artifactConfig: JavaRestClientJDKGeneratorConfig, fqn: (type: string) => string) { - if( isMKeyProperty(property) || isMRevisionProperty(property) ) { - node.append('@Override', NL) - node.append(`public Builder ${property.name}(${builtinToJavaType(property.type, fqn)} ${property.name}) {`, NL) - node.indent( methodBody => { - methodBody.append(`${builtinBuilderAccess(property)};`, NL) - methodBody.append('return this;', NL) - }) - node.append('}', NL) - } else { - node.append('@Override', NL) - node.append(`public Builder ${property.name}(${toType(property, artifactConfig, fqn)} ${property.name}) {`, NL) - node.indent( methodBody => { - if( property.array ) { - if( property.variant === 'builtin' && isMBuiltinType(property.type) ) { - methodBody.append(`${builtinBuilderArrayJSONAccess({ type: property.type, name: property.name })});`, NL) - } else if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { - methodBody.append(`builder.add("${property.name}", DTOUtils.toJsonLiteralArray(${property.name}));`, NL); - } else { - methodBody.append(`builder.add("${property.name}", DTOUtils.toJsonObjectArray(${property.name}));`, NL); - } - } else { - if( property.variant === 'builtin' && isMBuiltinType(property.type) ) { - methodBody.append(`${builtinBuilderAccess({ type: property.type, name: property.name })};`, NL) - } else if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { - methodBody.append(`builder.add("${property.name}", ${property.name}.toString());`, NL); - } else { - methodBody.append(`builder.add("${property.name}", ((BaseDTOImpl)${property.name}).data);`, NL); - } - - } - methodBody.append('return this;', NL) - }); - node.append('}', NL) - } -} - -function generateProperty(node: IndentNode, property: MKeyProperty | MRevisionProperty | MProperty, artifactConfig: JavaRestClientJDKGeneratorConfig, fqn: (type: string) => string) { - if( isMKeyProperty(property) || isMRevisionProperty(property) ) { - node.append('@Override', NL) - node.append(`public ${builtinToJavaType(property.type, fqn)} ${property.name}() {`, NL) - node.indent( methodBody => { - methodBody.append(`return ${builtinSimpleJSONAccess(property)};`, NL) - }) - node.append('}', NL) - } else { - node.append('@Override', NL) - node.append(`public ${toType(property, artifactConfig, fqn)} ${property.name}() {`, NL) - node.indent( methodBody => { - if( property.array ) { - if( property.variant === 'builtin' && isMBuiltinType(property.type) ) { - methodBody.append(`return ${builtinArrayJSONAccess( { type: property.type, name: property.name }, fqn )};`,NL) - } else if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { - if( property.variant === 'enum' || property.variant === 'inline-enum') { - methodBody.append(`DTOUtils.mapLiterals(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::valueOf, List.of())`, NL); - } else { - methodBody.append(`DTOUtils.mapLiterals(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::of, List.of())`, NL); - } - } else { - methodBody.append(`DTOUtils.mapObjects(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::of, List.of())`, NL); - } - - } else { - if( property.variant === 'builtin' && isMBuiltinType(property.type) ) { - if( property.nullable || property.optional ) { - methodBody.append(`return ${builtinOptionalJSONAccess( { type: property.type, name: property.name })};`, NL); - } else { - methodBody.append(`return ${builtinSimpleJSONAccess( { type: property.type, name: property.name })};`, NL); - } - } else { - if( property.nullable || property.optional ) { - if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { - if( property.variant === 'enum' || property.variant === 'inline-enum') { - methodBody.append(`return DTOUtils.mapLiteral(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::valueOf, ${toType(property, artifactConfig, fqn)}.values()[0])`, NL); - } else { - methodBody.append(`return DTOUtils.mapLiteral(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::of, null)`, NL); - } - } else { - methodBody.append(`return DTOUtils.mapObject(data, "${property.name}", ${property.type}DTOImpl::of, null);`, NL); - } - } else { - if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { - if( property.variant === 'enum' || property.variant === 'inline-enum') { - methodBody.append(`return DTOUtils.mapLiteral(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::valueOf)`, NL); - } else { - methodBody.append(`return DTOUtils.mapLiteral(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::of)`, NL); - } - } else { - methodBody.append(`return DTOUtils.mapObject(data, "${property.name}", ${property.type}DTOImpl::of);`, NL); - } - } - } - } - } ) - node.append('}', NL) - } -} - -function builtinBuilderArrayJSONAccess(property: { type: MBuiltinType, name: string }): string { - switch(property.type) { - case 'boolean': return `builder.add("${property.name}", DTOUtils.toJsonBooleanArray(${property.name})`; - case 'double': return `builder.add("${property.name}", DTOUtils.toJsonDoubleArray(${property.name})`; - case 'float': return `builder.add("${property.name}", DTOUtils.toJsonFloatArray(${property.name})`; - case 'int': return `builder.add("${property.name}", DTOUtils.toJsonIntArray(${property.name})`; - case 'local-date': return `builder.add("${property.name}", DTOUtils.toJsonLiteralArray(${property.name})`; - case 'local-date-time': return `builder.add("${property.name}", DTOUtils.toJsonLiteralArray(${property.name})::toString)`; - case 'long': return `builder.add("${property.name}", DTOUtils.toJsonLongArray(${property.name})`; - case 'short': return `builder.add("${property.name}", DTOUtils.toJsonShortArray(${property.name})`; - case 'string': return `builder.add("${property.name}", DTOUtils.toJsonStringArray(${property.name})`; - case 'zoned-date-time': return `builder.add("${property.name}", DTOUtils.toJsonLiteralArray(${property.name})`; - } -} - - -function builtinBuilderAccess(property: { type: MBuiltinType, name: string }): string { - switch(property.type) { - case 'boolean': - case 'double': - case 'float': - case 'int': - case 'long': - case 'short': - case 'string': - return `builder.add("${property.name}", ${property.name})`; - case 'local-date': - case 'local-date-time': - case 'zoned-date-time': - return `builder.add("${property.name}", ${property.name}.toString())`; - } -} - -function builtinArrayJSONAccess(property: { type: MBuiltinType, name: string }, fqn: (type: string) => string): string { - switch(property.type) { - case 'boolean': return `DTOUtils.mapBooleans(data, "${property.name}")`; - case 'double': return `DTOUtils.mapDoubles(data, "${property.name}")`; - case 'float': return `DTOUtils.mapFloats(data, "${property.name}")`; - case 'int': return `DTOUtils.mapInts(data, "${property.name}")`; - case 'local-date': return `DTOUtils.mapLiterals(data, "${property.name}", ${fqn('java.time.LocalDate')}::parse)`; - case 'local-date-time': return `DTOUtils.mapLiterals(data, "${property.name}, ${fqn('java.time.LocalDateTime')}::parse)`; - case 'long': return `DTOUtils.mapLongs(data, "${property.name}")`; - case 'short': return `DTOUtils.mapShorts(data, "${property.name}")`; - case 'string': return `DTOUtils.mapStrings(data, "${property.name}")`; - case 'zoned-date-time': return `DTOUtils.mapLiterals(data, "${property.name}, ${fqn('java.time.ZonedDateTime')}::parse)`; - } -} - - -function builtinSimpleJSONAccess(property: { type: MBuiltinType, name: string }): string { - switch(property.type) { - case 'boolean': return `DTOUtils.mapBoolean(data, "${property.name}")`; - case 'double': return `DTOUtils.mapDouble(data, "${property.name}")`; - case 'float': return `DTOUtils.mapFloat(data, "${property.name}")`; - case 'int': return `DTOUtils.mapInt(data, "${property.name}")`; - case 'local-date': return `DTOUtils.mapLocalDate(data, "${property.name}")`; - case 'local-date-time': return `DTOUtils.mapLocalDateTime(data, "${property.name}")`; - case 'long': return `DTOUtils.mapLong(data, "${property.name}")`; - case 'short': return `DTOUtils.mapShort(data, "${property.name}")`; - case 'string': return `DTOUtils.mapString(data, "${property.name}")`; - case 'zoned-date-time': return `DTOUtils.mapZonedDateTime(data, "${property.name}")`; - } -} - -function builtinOptionalJSONAccess(property: { type: MBuiltinType, name: string }): string { - switch(property.type) { - case 'boolean': return `DTOUtils.mapBoolean(data, "${property.name}", false)`; - case 'double': return `DTOUtils.mapDouble(data, "${property.name}", 0)`; - case 'float': return `DTOUtils.mapFloat(data, "${property.name}", 0)`; - case 'int': return `DTOUtils.mapInt(data, "${property.name}", 0)`; - case 'local-date': return `DTOUtils.mapLocalDate(data, "${property.name}", null)`; - case 'local-date-time': return `DTOUtils.mapLocalDateTime(data, "${property.name}", null)`; - case 'long': return `DTOUtils.mapLong(data, "${property.name}", 0)`; - case 'short': return `DTOUtils.mapShort(data, "${property.name}", (short) 0)`; - case 'string': return `DTOUtils.mapString(data, "${property.name}", null)`; - case 'zoned-date-time': return `DTOUtils.mapZonedDateTime(data, "${property.name}", null)`; - } -} \ No newline at end of file diff --git a/dsl/src/cli/java-rest-client-jdk/shared.ts b/dsl/src/cli/java-rest-client-jdk/shared.ts new file mode 100644 index 0000000..aca4368 --- /dev/null +++ b/dsl/src/cli/java-rest-client-jdk/shared.ts @@ -0,0 +1,182 @@ +import { IndentNode, NL } from "langium/generate" +import { isMBuiltinType, isMKeyProperty, isMRevisionProperty, MBuiltinType, MKeyProperty, MProperty, MRevisionProperty } from "../model.js" +import { builtinToJavaType, JavaRestClientJDKGeneratorConfig, resolveType } from "../java-gen-utils.js" +import { toType } from "../java-client-api/shared.js" +import { toFirstUpper } from "../util.js" + +export function generateProperty(node: IndentNode, property: MKeyProperty | MRevisionProperty | MProperty, artifactConfig: JavaRestClientJDKGeneratorConfig, fqn: (type: string) => string) { + if( isMKeyProperty(property) || isMRevisionProperty(property) ) { + node.append('@Override', NL) + node.append(`public ${builtinToJavaType(property.type, fqn)} ${property.name}() {`, NL) + node.indent( methodBody => { + methodBody.append(`return ${builtinSimpleJSONAccess(property)};`, NL) + }) + node.append('}', NL) + } else { + node.append('@Override', NL) + node.append(`public ${toType(property, artifactConfig, fqn)} ${property.name}() {`, NL) + node.indent( methodBody => { + if( property.array ) { + if( property.variant === 'builtin' && isMBuiltinType(property.type) ) { + methodBody.append(`return ${builtinArrayJSONAccess( { type: property.type, name: property.name }, fqn )};`,NL) + } else if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { + if( property.variant === 'enum' || property.variant === 'inline-enum') { + const type = typeof property.type === 'string' ? property.type : toFirstUpper(property.name); + methodBody.append(`return DTOUtils.mapLiterals(data, "${property.name}", ${resolveType(type, artifactConfig.nativeTypeSubstitues, fqn)}::valueOf);`, NL); + } else if( typeof property.type === 'string' ) { + methodBody.append(`return DTOUtils.mapLiterals(data, "${property.name}", ${resolveType(property.type, artifactConfig.nativeTypeSubstitues, fqn)}::of);`, NL); + } + } else { + methodBody.append(`DTOUtils.mapObjects(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::of)`, NL); + } + + } else { + if( property.variant === 'builtin' && isMBuiltinType(property.type) ) { + if( property.nullable || property.optional ) { + methodBody.append(`return ${builtinOptionalJSONAccess( { type: property.type, name: property.name })};`, NL); + } else { + methodBody.append(`return ${builtinSimpleJSONAccess( { type: property.type, name: property.name })};`, NL); + } + } else { + if( property.nullable || property.optional ) { + if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { + if( property.variant === 'enum' || property.variant === 'inline-enum') { + methodBody.append(`return DTOUtils.mapLiteral(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::valueOf, ${toType(property, artifactConfig, fqn)}.values()[0]);`, NL); + } else { + methodBody.append(`return DTOUtils.mapLiteral(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::of, null);`, NL); + } + } else { + methodBody.append(`return DTOUtils.mapObject(data, "${property.name}", ${property.type}DTOImpl::of, null);`, NL); + } + } else { + if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { + if( property.variant === 'enum' || property.variant === 'inline-enum') { + methodBody.append(`return DTOUtils.mapLiteral(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::valueOf);`, NL); + } else { + methodBody.append(`return DTOUtils.mapLiteral(data, "${property.name}", ${toType(property, artifactConfig, fqn)}::of);`, NL); + } + } else { + methodBody.append(`return DTOUtils.mapObject(data, "${property.name}", ${property.type}DTOImpl::of);`, NL); + } + } + } + } + } ) + node.append('}', NL) + } +} + +function builtinArrayJSONAccess(property: { type: MBuiltinType, name: string }, fqn: (type: string) => string): string { + switch(property.type) { + case 'boolean': return `DTOUtils.mapBooleans(data, "${property.name}")`; + case 'double': return `DTOUtils.mapDoubles(data, "${property.name}")`; + case 'float': return `DTOUtils.mapFloats(data, "${property.name}")`; + case 'int': return `DTOUtils.mapInts(data, "${property.name}")`; + case 'local-date': return `DTOUtils.mapLiterals(data, "${property.name}", ${fqn('java.time.LocalDate')}::parse)`; + case 'local-date-time': return `DTOUtils.mapLiterals(data, "${property.name}, ${fqn('java.time.LocalDateTime')}::parse)`; + case 'long': return `DTOUtils.mapLongs(data, "${property.name}")`; + case 'short': return `DTOUtils.mapShorts(data, "${property.name}")`; + case 'string': return `DTOUtils.mapStrings(data, "${property.name}")`; + case 'zoned-date-time': return `DTOUtils.mapLiterals(data, "${property.name}, ${fqn('java.time.ZonedDateTime')}::parse)`; + } +} + + +function builtinSimpleJSONAccess(property: { type: MBuiltinType, name: string }): string { + switch(property.type) { + case 'boolean': return `DTOUtils.mapBoolean(data, "${property.name}")`; + case 'double': return `DTOUtils.mapDouble(data, "${property.name}")`; + case 'float': return `DTOUtils.mapFloat(data, "${property.name}")`; + case 'int': return `DTOUtils.mapInt(data, "${property.name}")`; + case 'local-date': return `DTOUtils.mapLocalDate(data, "${property.name}")`; + case 'local-date-time': return `DTOUtils.mapLocalDateTime(data, "${property.name}")`; + case 'long': return `DTOUtils.mapLong(data, "${property.name}")`; + case 'short': return `DTOUtils.mapShort(data, "${property.name}")`; + case 'string': return `DTOUtils.mapString(data, "${property.name}")`; + case 'zoned-date-time': return `DTOUtils.mapZonedDateTime(data, "${property.name}")`; + } +} + +function builtinOptionalJSONAccess(property: { type: MBuiltinType, name: string }): string { + switch(property.type) { + case 'boolean': return `DTOUtils.mapBoolean(data, "${property.name}", false)`; + case 'double': return `DTOUtils.mapDouble(data, "${property.name}", 0)`; + case 'float': return `DTOUtils.mapFloat(data, "${property.name}", 0)`; + case 'int': return `DTOUtils.mapInt(data, "${property.name}", 0)`; + case 'local-date': return `DTOUtils.mapLocalDate(data, "${property.name}", null)`; + case 'local-date-time': return `DTOUtils.mapLocalDateTime(data, "${property.name}", null)`; + case 'long': return `DTOUtils.mapLong(data, "${property.name}", 0)`; + case 'short': return `DTOUtils.mapShort(data, "${property.name}", (short) 0)`; + case 'string': return `DTOUtils.mapString(data, "${property.name}", null)`; + case 'zoned-date-time': return `DTOUtils.mapZonedDateTime(data, "${property.name}", null)`; + } +} + +export function generateBuilderProperty(node: IndentNode, property: MKeyProperty | MRevisionProperty | MProperty, artifactConfig: JavaRestClientJDKGeneratorConfig, fqn: (type: string) => string, typePrefix?: string) { + if( isMKeyProperty(property) || isMRevisionProperty(property) ) { + node.append('@Override', NL) + node.append(`public ${typePrefix ? `${typePrefix}.`: ''}Builder ${property.name}(${builtinToJavaType(property.type, fqn)} ${property.name}) {`, NL) + node.indent( methodBody => { + methodBody.append(`${builtinBuilderAccess(property)};`, NL) + methodBody.append('return this;', NL) + }) + node.append('}', NL) + } else { + node.append('@Override', NL) + node.append(`public ${typePrefix ? `${typePrefix}.`: ''}Builder ${property.name}(${toType(property, artifactConfig, fqn)} ${property.name}) {`, NL) + node.indent( methodBody => { + if( property.array ) { + if( property.variant === 'builtin' && isMBuiltinType(property.type) ) { + methodBody.append(`${builtinBuilderArrayJSONAccess({ type: property.type, name: property.name })});`, NL) + } else if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { + methodBody.append(`builder.add("${property.name}", DTOUtils.toJsonLiteralArray(${property.name}));`, NL); + } else { + methodBody.append(`builder.add("${property.name}", DTOUtils.toJsonObjectArray(${property.name}));`, NL); + } + } else { + if( property.variant === 'builtin' && isMBuiltinType(property.type) ) { + methodBody.append(`${builtinBuilderAccess({ type: property.type, name: property.name })};`, NL) + } else if( property.variant === 'enum' || property.variant === 'inline-enum' || property.variant === 'scalar' ) { + methodBody.append(`builder.add("${property.name}", ${property.name}.toString());`, NL); + } else { + methodBody.append(`builder.add("${property.name}", ((BaseDTOImpl)${property.name}).data);`, NL); + } + + } + methodBody.append('return this;', NL) + }); + node.append('}', NL) + } +} + +function builtinBuilderArrayJSONAccess(property: { type: MBuiltinType, name: string }): string { + switch(property.type) { + case 'boolean': return `builder.add("${property.name}", DTOUtils.toJsonBooleanArray(${property.name})`; + case 'double': return `builder.add("${property.name}", DTOUtils.toJsonDoubleArray(${property.name})`; + case 'float': return `builder.add("${property.name}", DTOUtils.toJsonFloatArray(${property.name})`; + case 'int': return `builder.add("${property.name}", DTOUtils.toJsonIntArray(${property.name})`; + case 'local-date': return `builder.add("${property.name}", DTOUtils.toJsonLiteralArray(${property.name})`; + case 'local-date-time': return `builder.add("${property.name}", DTOUtils.toJsonLiteralArray(${property.name})::toString)`; + case 'long': return `builder.add("${property.name}", DTOUtils.toJsonLongArray(${property.name})`; + case 'short': return `builder.add("${property.name}", DTOUtils.toJsonShortArray(${property.name})`; + case 'string': return `builder.add("${property.name}", DTOUtils.toJsonStringArray(${property.name})`; + case 'zoned-date-time': return `builder.add("${property.name}", DTOUtils.toJsonLiteralArray(${property.name})`; + } +} + +function builtinBuilderAccess(property: { type: MBuiltinType, name: string }): string { + switch(property.type) { + case 'boolean': + case 'double': + case 'float': + case 'int': + case 'long': + case 'short': + case 'string': + return `builder.add("${property.name}", ${property.name})`; + case 'local-date': + case 'local-date-time': + case 'zoned-date-time': + return `builder.add("${property.name}", ${property.name}.toString())`; + } +} diff --git a/dsl/src/cli/java-rest-client-jdk/union.ts b/dsl/src/cli/java-rest-client-jdk/union.ts new file mode 100644 index 0000000..4042114 --- /dev/null +++ b/dsl/src/cli/java-rest-client-jdk/union.ts @@ -0,0 +1,142 @@ +import { CompositeGeneratorNode, NL, toString } from "langium/generate"; +import { Artifact } from "../artifact-generator.js"; +import { builtinToJavaType, generateCompilationUnit, JavaImportsCollector, JavaRestClientJDKGeneratorConfig, toPath } from "../java-gen-utils.js"; +import { isMKeyProperty, isMRevisionProperty, MResolvedUnionType } from "../model.js"; +import { generateBuilderProperty, generateProperty } from "./shared.js"; +import { toType } from "../java-client-api/shared.js"; + +export function generateUnion(t: MResolvedUnionType, artifactConfig: JavaRestClientJDKGeneratorConfig): Artifact { + const packageName = `${artifactConfig.rootPackageName}.jdkhttp.impl.dto`; + + const importCollector = new JavaImportsCollector(packageName); + const fqn = importCollector.importType.bind(importCollector); + + return { + name: `${t.name}DTOImpl.java`, + content: toString(generateCompilationUnit(packageName, importCollector, generateUnionContent(t, artifactConfig, fqn))), + path: toPath(artifactConfig.targetFolder, packageName) + }; +} + +function generateUnionContent(t: MResolvedUnionType, artifactConfig: JavaRestClientJDKGeneratorConfig, fqn: (type: string) => string) { + const node = new CompositeGeneratorNode(); + + const DTOInterface = fqn(`${artifactConfig.rootPackageName}.dto.${t.name}DTO`) + const JsonObject = fqn('jakarta.json.JsonObject') + const JsonArray = fqn('jakarta.json.JsonArray') + const Json = fqn('jakarta.json.Json') + const JsonObjectBuilder = fqn('jakarta.json.JsonObjectBuilder'); + node.append(`public abstract class ${t.name}DTOImpl extends BaseDTOImpl implements ${DTOInterface} {`, NL) + + node.indent( classBody => { + classBody.append(`${t.name}DTOImpl(${JsonObject} data) {`, NL); + classBody.indent( initBody => { + initBody.append('super(data);', NL) + }) + classBody.append('}', NL) + + if( t.resolved.sharedProps.length > 0 ) { + t.resolved.sharedProps.forEach( p => { + classBody.appendNewLine(); + generateProperty(classBody, p, artifactConfig, fqn); + } ) + } + + classBody.appendNewLine(); + classBody.append(`public static ${t.name}DTO of(${JsonObject} data) {`, NL) + classBody.indent( methodBody => { + methodBody.append(`var descriminator = data.getString("${t.descriminator}");`, NL) + methodBody.append(`return switch(descriminator) {`, NL) + methodBody.indent( switchBlock => { + t.types.forEach( subtype => { + const key = (t.descriminatorAliases ?? {})[subtype] ?? subtype; + switchBlock.append(`case "${key}" -> new ${subtype}DTOImpl(data);`, NL); + } ) + switchBlock.append(`default -> throw new IllegalArgumentException("Unexpected value: %s".formatted(descriminator));`, NL); + }); + methodBody.append('};',NL) + }); + classBody.append('}',NL); + classBody.appendNewLine() + classBody.append(`public static ${fqn('java.util.List')}<${t.name}DTO> of(${JsonArray} data) {`, NL) + classBody.indent( methodBody => { + methodBody.append(`return DTOUtils.mapObjects(data, ${t.name}DTOImpl::of);`, NL) + }); + classBody.append('}', NL) + const keyProp = t.resolved.sharedProps.find( e => e["@type"] === 'KeyProperty'); + if( keyProp ) { + classBody.appendNewLine(); + classBody.append('@Override',NL) + classBody.append('public String toString() {',NL) + classBody.indent( methodBody => { + methodBody.append(`return "%s[%s=%s]".formatted(getClass().getSimpleName(), "${keyProp.name}", ${keyProp.name}());`,NL) + }); + classBody.append('}',NL) + } + classBody.appendNewLine() + classBody.append(`public static abstract class BuilderImpl implements ${t.name}DTO.Builder {`, NL) + classBody.indent( builderBody => { + builderBody.append(`protected final ${JsonObjectBuilder} builder = ${Json}.createObjectBuilder();`, NL) + t.resolved.sharedProps.forEach( p => { + builderBody.appendNewLine(); + generateBuilderProperty(builderBody, p, artifactConfig, fqn); + }); + }) + classBody.append('}', NL) + t.resolved.records.forEach( subtype => { + classBody.appendNewLine(); + classBody.append(`public static class ${subtype.name}DTOImpl extends ${t.name}DTOImpl implements ${subtype.name}DTO {`, NL) + classBody.indent( subtypeBody => { + subtypeBody.append(`${subtype.name}DTOImpl(${JsonObject} data) {`, NL); + subtypeBody.indent( initBody => { + initBody.append('super(data);', NL) + }) + subtypeBody.append('}', NL) + + subtype.properties.forEach( prop => { + subtypeBody.appendNewLine(); + generateProperty(subtypeBody, prop, artifactConfig, fqn); + }); + + subtypeBody.append(`public static class BuilderImpl extends ${t.name}DTOImpl.BuilderImpl<${subtype.name}DTO> implements ${subtype.name}DTO.Builder {`, NL) + subtypeBody.indent( subtypeBuilderBody => { + t.resolved.sharedProps.forEach( property => { + subtypeBuilderBody.appendNewLine(); + if( isMKeyProperty(property) || isMRevisionProperty(property) ) { + subtypeBuilderBody.append('@Override', NL) + subtypeBuilderBody.append(`public ${subtype.name}DTO.Builder ${property.name}(${builtinToJavaType(property.type, fqn)} ${property.name}) {`, NL) + subtypeBuilderBody.indent( methodBody => { + methodBody.append(`return (${subtype.name}DTO.Builder) super.${property.name}(${property.name});`, NL) + }) + subtypeBuilderBody.append('}', NL) + } else { + subtypeBuilderBody.append('@Override', NL) + subtypeBuilderBody.append(`public ${subtype.name}DTO.Builder ${property.name}(${toType(property, artifactConfig, fqn)} ${property.name}) {`, NL) + subtypeBuilderBody.indent( methodBody => { + methodBody.append(`return (${subtype.name}DTO.Builder) super.${property.name}(${property.name});`, NL) + }) + subtypeBuilderBody.append('}', NL); + } + } ) + subtype.properties.forEach( prop => { + subtypeBuilderBody.appendNewLine(); + generateBuilderProperty(subtypeBuilderBody, prop, artifactConfig, fqn, `${subtype.name}DTO`); + }) + subtypeBuilderBody.appendNewLine(); + subtypeBuilderBody.append(`public ${subtype.name}DTO build() {`, NL) + subtypeBuilderBody.indent( methodBody => { + methodBody.append(`return new ${subtype.name}DTOImpl(builder.build());`,NL); + }) + subtypeBuilderBody.append('}', NL) + } ) + + subtypeBody.append('}', NL) + }); + classBody.append('}', NL) + }); + }); + + node.append('}', NL) + + return node; +} \ No newline at end of file diff --git a/dsl/src/cli/model.ts b/dsl/src/cli/model.ts index f7e2c1b..6fca76e 100644 --- a/dsl/src/cli/model.ts +++ b/dsl/src/cli/model.ts @@ -68,6 +68,10 @@ export function isMUnionType(value: unknown): value is MUnionType { && value['@type'] === 'UnionType'; } +export function isMResolvedUnionType(value: unknown): value is MResolvedUnionType { + return isMUnionType(value) && 'resolved' in value && isObject(value.resolved); +} + export type MMixinType = { '@type': 'MixinType' name: string