From bbb340d50faaa48652e7cf037967e37480ca554f Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 17:36:24 +0100 Subject: [PATCH 01/45] chore: ignore .idea settings --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4c9e690918..f89e1b2643 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules coverage lib *.DS_Store -.vscode \ No newline at end of file +.vscode +.idea From 60a601979c695ebbe8920f1b1582577f4ccb0b51 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 17:36:46 +0100 Subject: [PATCH 02/45] wip: enum generator seems to work --- examples/generate-kotlin-enums/README.md | 17 ++ examples/generate-kotlin-enums/index.spec.ts | 13 ++ examples/generate-kotlin-enums/index.ts | 20 ++ .../generate-kotlin-enums/package-lock.json | 10 + examples/generate-kotlin-enums/package.json | 12 + output | 35 +++ src/generators/index.ts | 1 + src/generators/kotlin/Constants.ts | 62 +++++ src/generators/kotlin/KotlinConstrainer.ts | 142 ++++++++++++ src/generators/kotlin/KotlinFileGenerator.ts | 25 ++ src/generators/kotlin/KotlinGenerator.ts | 121 ++++++++++ src/generators/kotlin/KotlinPreset.ts | 18 ++ src/generators/kotlin/KotlinRenderer.ts | 29 +++ .../kotlin/constrainer/EnumConstrainer.ts | 59 +++++ .../constrainer/ModelNameConstrainer.ts | 41 ++++ .../constrainer/PropertyKeyConstrainer.ts | 47 ++++ src/generators/kotlin/index.ts | 21 ++ .../kotlin/presets/DescriptionPreset.ts | 25 ++ src/generators/kotlin/presets/index.ts | 1 + .../kotlin/renderers/ClassRenderer.ts | 89 ++++++++ .../kotlin/renderers/EnumRenderer.ts | 52 +++++ test/TestUtils/TestRenderers.ts | 3 + test/generators/kotlin/Constants.spec.ts | 11 + .../kotlin/KotlinConstrainer.spec.ts | 215 ++++++++++++++++++ .../generators/kotlin/KotlinGenerator.spec.ts | 132 +++++++++++ test/generators/kotlin/KotlinRenderer.spec.ts | 19 ++ .../KotlinGenerator.spec.ts.snap | 166 ++++++++++++++ .../kotlin/presets/DescriptionPreset.spec.ts | 40 ++++ .../DescriptionPreset.spec.ts.snap | 31 +++ 29 files changed, 1457 insertions(+) create mode 100644 examples/generate-kotlin-enums/README.md create mode 100644 examples/generate-kotlin-enums/index.spec.ts create mode 100644 examples/generate-kotlin-enums/index.ts create mode 100644 examples/generate-kotlin-enums/package-lock.json create mode 100644 examples/generate-kotlin-enums/package.json create mode 100644 output create mode 100644 src/generators/kotlin/Constants.ts create mode 100644 src/generators/kotlin/KotlinConstrainer.ts create mode 100644 src/generators/kotlin/KotlinFileGenerator.ts create mode 100644 src/generators/kotlin/KotlinGenerator.ts create mode 100644 src/generators/kotlin/KotlinPreset.ts create mode 100644 src/generators/kotlin/KotlinRenderer.ts create mode 100644 src/generators/kotlin/constrainer/EnumConstrainer.ts create mode 100644 src/generators/kotlin/constrainer/ModelNameConstrainer.ts create mode 100644 src/generators/kotlin/constrainer/PropertyKeyConstrainer.ts create mode 100644 src/generators/kotlin/index.ts create mode 100644 src/generators/kotlin/presets/DescriptionPreset.ts create mode 100644 src/generators/kotlin/presets/index.ts create mode 100644 src/generators/kotlin/renderers/ClassRenderer.ts create mode 100644 src/generators/kotlin/renderers/EnumRenderer.ts create mode 100644 test/generators/kotlin/Constants.spec.ts create mode 100644 test/generators/kotlin/KotlinConstrainer.spec.ts create mode 100644 test/generators/kotlin/KotlinGenerator.spec.ts create mode 100644 test/generators/kotlin/KotlinRenderer.spec.ts create mode 100644 test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap create mode 100644 test/generators/kotlin/presets/DescriptionPreset.spec.ts create mode 100644 test/generators/kotlin/presets/__snapshots__/DescriptionPreset.spec.ts.snap diff --git a/examples/generate-kotlin-enums/README.md b/examples/generate-kotlin-enums/README.md new file mode 100644 index 0000000000..d9b12e1b38 --- /dev/null +++ b/examples/generate-kotlin-enums/README.md @@ -0,0 +1,17 @@ +# Java Data Models + +A basic example of how to use Modelina and output a Kotlin enum. + +## How to run this example + +Run this example using: + +```sh +npm i && npm run start +``` + +If you are on Windows, use the `start:windows` script instead: + +```sh +npm i && npm run start:windows +``` diff --git a/examples/generate-kotlin-enums/index.spec.ts b/examples/generate-kotlin-enums/index.spec.ts new file mode 100644 index 0000000000..3e6e222d60 --- /dev/null +++ b/examples/generate-kotlin-enums/index.spec.ts @@ -0,0 +1,13 @@ +const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; }); +import {generate} from './index'; + +describe('Should be able to render Java Models', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + test('and should log expected output to console', async () => { + await generate(); + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + }); +}); diff --git a/examples/generate-kotlin-enums/index.ts b/examples/generate-kotlin-enums/index.ts new file mode 100644 index 0000000000..a0db6cafb4 --- /dev/null +++ b/examples/generate-kotlin-enums/index.ts @@ -0,0 +1,20 @@ +import {JavaGenerator, KotlinGenerator} from '../../src'; + +const generator = new KotlinGenerator(); +const jsonSchemaDraft7 = { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + $id: 'protocol', + type: ['string', 'int', 'boolean'], + enum: ['HTTP', 1, 'HTTPS', true], +}; + +export async function generate() : Promise { + const models = await generator.generate(jsonSchemaDraft7); + for (const model of models) { + console.log(model.result); + } +} +if (require.main === module) { + generate(); +} diff --git a/examples/generate-kotlin-enums/package-lock.json b/examples/generate-kotlin-enums/package-lock.json new file mode 100644 index 0000000000..713584b29b --- /dev/null +++ b/examples/generate-kotlin-enums/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "generate-kotlin-enums", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/generate-kotlin-enums/package.json b/examples/generate-kotlin-enums/package.json new file mode 100644 index 0000000000..d14efa1d59 --- /dev/null +++ b/examples/generate-kotlin-enums/package.json @@ -0,0 +1,12 @@ +{ + "config": { + "example_name": "generate-kotlin-enums" + }, + "scripts": { + "install": "cd ../.. && npm i", + "start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts", + "start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts", + "test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts", + "test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts" + } +} diff --git a/output b/output new file mode 100644 index 0000000000..37884a89b3 --- /dev/null +++ b/output @@ -0,0 +1,35 @@ + + + public class Root { + private Email email; + + public Email getEmail() { return this.email; } + public void setEmail(Email email) { this.email = email; } +} +public enum Email { + EXAMPLE1_AT_TEST_DOT_COM((String)"example1@test.com"), EXAMPLE2_AT_TEST_DOT_COM((String)"example2@test.com"); + + private String value; + + Email(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static Email fromValue(String value) { + for (Email e : Email.values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + + @Override + public String toString() { + return String.valueOf(value); + } +} \ No newline at end of file diff --git a/src/generators/index.ts b/src/generators/index.ts index f5b242b2a8..f26efad2f1 100644 --- a/src/generators/index.ts +++ b/src/generators/index.ts @@ -8,4 +8,5 @@ export * from './typescript'; export * from './python'; export * from './go'; export * from './rust'; +export * from './kotlin'; export * from './AbstractFileGenerator'; diff --git a/src/generators/kotlin/Constants.ts b/src/generators/kotlin/Constants.ts new file mode 100644 index 0000000000..693ee7bf27 --- /dev/null +++ b/src/generators/kotlin/Constants.ts @@ -0,0 +1,62 @@ +/** + * For the full list of Kotlin keywords, refer to the reference documentation. + * https://kotlinlang.org/docs/keyword-reference.html + */ + +import { checkForReservedKeyword } from '../../helpers'; + +export const RESERVED_KEYWORDS_ILLEGAL_AS_PARAMETER = [ + 'as', + 'as?', + 'break', + 'class', + 'continue', + 'do', + 'else', + 'false', + 'for', + 'fun', + 'if', + 'in', + '!in', + 'interface', + 'is', + '!is', + 'null', + 'object', + 'package', + 'return', + 'super', + 'this', + 'throw', + 'true', + 'try', + 'typealias', + 'typeof', + 'val', + 'var', + 'when', + 'while' +]; + +export const ILLEGAL_ENUM_FIELDS = [ + 'as?', + '!in', + '!is' +]; + +export function isInvalidKotlinEnumKey(word: string): boolean { + return checkForReservedKeyword( + word, + ILLEGAL_ENUM_FIELDS, + true + ); +} + +export function isReservedKotlinKeyword(word: string, forceLowerCase = true): boolean { + return checkForReservedKeyword( + word, + RESERVED_KEYWORDS_ILLEGAL_AS_PARAMETER, + forceLowerCase + ); +} diff --git a/src/generators/kotlin/KotlinConstrainer.ts b/src/generators/kotlin/KotlinConstrainer.ts new file mode 100644 index 0000000000..267c8d13c8 --- /dev/null +++ b/src/generators/kotlin/KotlinConstrainer.ts @@ -0,0 +1,142 @@ +import { ConstrainedEnumValueModel, MetaModel } from 'models'; +import { TypeMapping } from '../../helpers'; +import { defaultEnumKeyConstraints, defaultEnumValueConstraints } from './constrainer/EnumConstrainer'; +import { defaultModelNameConstraints } from './constrainer/ModelNameConstrainer'; +import { defaultPropertyKeyConstraints } from './constrainer/PropertyKeyConstrainer'; +import { KotlinOptions } from './KotlinGenerator'; + +function enumFormatToNumberType(enumValueModel: ConstrainedEnumValueModel, format: string): string { + switch (format) { + case 'integer': + case 'int32': + return 'Int'; + case 'long': + case 'int64': + return 'Long'; + case 'float': + return 'Float'; + case 'double': + return 'Double'; + default: + return Number.isInteger(enumValueModel.value) ? 'Int' : 'Double'; + } +} + +function fromEnumValueToKotlinType(enumValueModel: ConstrainedEnumValueModel, format: string): string { + switch (typeof enumValueModel.value) { + case 'boolean': + return 'Boolean'; + case 'number': + case 'bigint': + return enumFormatToNumberType(enumValueModel, format); + case 'object': + return 'Any'; + case 'string': + return 'String'; + default: + return 'Any'; + } +} + +/** + * Converts union of different number types to the most strict type it can be. + * + * int + double = double (long + double, float + double can never happen, otherwise this would be converted to double) + * int + float = float (long + float can never happen, otherwise this would be the case as well) + * int + long = long + * + * Basically a copy from JavaConstrainer.ts + */ +function interpretUnionValueType(types: string[]): string { + if (types.includes('Double')) { + return 'Double'; + } + + if (types.includes('Float')) { + return 'Float'; + } + + if (types.includes('Long')) { + return 'Long'; + } + + return 'Any'; +} + +export const KotlinDefaultTypeMapping: TypeMapping = { + Object ({constrainedModel}): string { + return constrainedModel.name; + }, + Reference ({constrainedModel}): string { + return constrainedModel.name; + }, + Any (): string { + return 'Any'; + }, + Float ({ constrainedModel }): string { + const format = constrainedModel.originalInput && constrainedModel.originalInput['format'] + return format === 'float' ? 'Float' : 'Double'; + }, + Integer ({ constrainedModel }): string { + const format = constrainedModel.originalInput && constrainedModel.originalInput['format']; + return format === 'long' || format === 'int64' ? 'Long' : 'Int'; + }, + String ({ constrainedModel }): string { + const format = constrainedModel.originalInput && constrainedModel.originalInput['format'] + switch (format) { + case 'date': { + return 'java.time.LocalDate'; + } + case 'time': { + return 'java.time.OffsetTime'; + } + case 'dateTime': + case 'date-time': { + return 'java.time.OffsetDateTime'; + } + case 'binary': { + return 'ByteArray'; + } + default: { + return 'String'; + } + } + }, + Boolean (): string { + return 'Boolean'; + }, + // Since there are not tuples in Kotlin, we have to return a collection of `Any` + Tuple ({ options }): string { + const isList = options.collectionType && options.collectionType === 'List'; + + return isList ? 'List' : 'Array'; + }, + Array ({ constrainedModel, options }): string { + const isList = options.collectionType && options.collectionType === 'List'; + const type = constrainedModel.valueModel.type; + + return isList ? `List<${type}>` : `Array<${type}>`; + }, + Enum ({constrainedModel}): string { + const format = constrainedModel.originalInput && constrainedModel.originalInput['format']; + const valueTypes = constrainedModel.values.map((enumValue) => fromEnumValueToKotlinType(enumValue, format)); + const uniqueTypes = [...new Set(valueTypes)]; + + // Enums cannot handle union types, default to a loose type + return uniqueTypes.length > 1 ? interpretUnionValueType(uniqueTypes) : uniqueTypes[0]; + }, + Union (): string { + // No Unions in Kotlin, use Any + return 'Any'; + }, + Dictionary ({ constrainedModel }): string { + return `Map<${constrainedModel.key.type}, ${constrainedModel.value.type}>`; + } +}; + +export const KotlinDefaultConstraints = { + enumKey: defaultEnumKeyConstraints(), + enumValue: defaultEnumValueConstraints(), + modelName: defaultModelNameConstraints(), + propertyKey: defaultPropertyKeyConstraints() +}; diff --git a/src/generators/kotlin/KotlinFileGenerator.ts b/src/generators/kotlin/KotlinFileGenerator.ts new file mode 100644 index 0000000000..9814769b13 --- /dev/null +++ b/src/generators/kotlin/KotlinFileGenerator.ts @@ -0,0 +1,25 @@ +import { KotlinGenerator, KotlinRenderCompleteModelOptions } from '.'; +import { InputMetaModel, OutputModel } from '../../models'; +import * as path from 'path'; +import { AbstractFileGenerator } from '../AbstractFileGenerator'; +import { FileHelpers } from '../../helpers'; + +export class KotlinFileGenerator extends KotlinGenerator implements AbstractFileGenerator { + /** + * Generates all the models to an output directory each model with their own separate files. + * + * @param input + * @param outputDirectory where you want the models generated to + * @param options + */ + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: KotlinRenderCompleteModelOptions): Promise { + let generatedModels = await this.generateCompleteModels(input, options); + //Filter anything out that have not been successfully generated + generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); + for (const outputModel of generatedModels) { + const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.kt`); + await FileHelpers.writerToFileSystem(outputModel.result, filePath); + } + return generatedModels; + } +} diff --git a/src/generators/kotlin/KotlinGenerator.ts b/src/generators/kotlin/KotlinGenerator.ts new file mode 100644 index 0000000000..5f1aada5ce --- /dev/null +++ b/src/generators/kotlin/KotlinGenerator.ts @@ -0,0 +1,121 @@ +import { + AbstractGenerator, + CommonGeneratorOptions, + defaultGeneratorOptions +} from '../AbstractGenerator'; +import { ConstrainedEnumModel, ConstrainedMetaModel, ConstrainedObjectModel, InputMetaModel, MetaModel, RenderOutput } from '../../models'; +import {IndentationTypes, split, TypeMapping} from '../../helpers'; +import { KotlinPreset, KOTLIN_DEFAULT_PRESET } from './KotlinPreset'; +import { ClassRenderer } from './renderers/ClassRenderer'; +import { EnumRenderer } from './renderers/EnumRenderer'; +import { isReservedKotlinKeyword } from './Constants'; +import { Logger } from '../..'; +import { constrainMetaModel, Constraints } from '../../helpers/ConstrainHelpers'; +import { KotlinDefaultConstraints, KotlinDefaultTypeMapping } from './KotlinConstrainer'; +import { DeepPartial, mergePartialAndDefault } from '../../utils/Partials'; + +export interface KotlinOptions extends CommonGeneratorOptions { + typeMapping: TypeMapping; + constraints: Constraints; + collectionType: 'List' | 'Array'; +} +export interface KotlinRenderCompleteModelOptions { + packageName: string +} +export class KotlinGenerator extends AbstractGenerator { + static defaultOptions: KotlinOptions = { + ...defaultGeneratorOptions, + indentation: { + type: IndentationTypes.SPACES, + size: 4, + }, + defaultPreset: KOTLIN_DEFAULT_PRESET, + collectionType: 'List', + typeMapping: KotlinDefaultTypeMapping, + constraints: KotlinDefaultConstraints + }; + + constructor( + options?: DeepPartial, + ) { + const realizedOptions = mergePartialAndDefault(KotlinGenerator.defaultOptions, options) as KotlinOptions; + super('Kotlin', realizedOptions); + } + /** + * This function makes sure we split up the MetaModels accordingly to what we want to render as models. + */ + splitMetaModel(model: MetaModel): MetaModel[] { + const metaModelsToSplit = { + splitEnum: true, + splitObject: true + }; + return split(model, metaModelsToSplit); + } + + constrainToMetaModel(model: MetaModel): ConstrainedMetaModel { + return constrainMetaModel( + this.options.typeMapping, + this.options.constraints, + { + metaModel: model, + options: this.options, + constrainedName: '' //This is just a placeholder, it will be constrained within the function + } + ); + } + + /** + * Render a scattered model, where the source code and library and model dependencies are separated. + * + * @param model + * @param inputModel + */ + render(model: ConstrainedMetaModel, inputModel: InputMetaModel): Promise { + if (model instanceof ConstrainedObjectModel) { + return this.renderClass(model, inputModel); + } else if (model instanceof ConstrainedEnumModel) { + return this.renderEnum(model, inputModel); + } + Logger.warn(`Kotlin generator, cannot generate this type of model, ${model.name}`); + return Promise.resolve(RenderOutput.toRenderOutput({ result: '', renderedName: '', dependencies: [] })); + } + + /** + * Render a complete model result where the model code, library and model dependencies are all bundled appropriately. + * + * For Kotlin you need to specify which package the model is placed under. + * + * @param model + * @param inputModel + * @param options used to render the full output + */ + async renderCompleteModel(model: ConstrainedMetaModel, inputModel: InputMetaModel, options: KotlinRenderCompleteModelOptions): Promise { + if (isReservedKotlinKeyword(options.packageName)) { + throw new Error(`You cannot use reserved Kotlin keyword (${options.packageName}) as package name, please use another.`); + } + + const outputModel = await this.render(model, inputModel); + const modelDependencies = model.getNearestDependencies().map((dependencyModel) => { + return `import ${options.packageName}.${dependencyModel.name};`; + }); + const outputContent = `package ${options.packageName}; +${modelDependencies.join('\n')} +${outputModel.dependencies.join('\n')} +${outputModel.result}`; + return RenderOutput.toRenderOutput({result: outputContent, renderedName: outputModel.renderedName, dependencies: outputModel.dependencies}); + } + + async renderClass(model: ConstrainedObjectModel, inputModel: InputMetaModel): Promise { + const presets = this.getPresets('class'); + const renderer = new ClassRenderer(this.options, this, presets, model, inputModel); + const result = await renderer.runSelfPreset(); + return RenderOutput.toRenderOutput({result, renderedName: model.name, dependencies: renderer.dependencies}); + } + + async renderEnum(model: ConstrainedEnumModel, inputModel: InputMetaModel): Promise { + const presets = this.getPresets('enum'); + const renderer = new EnumRenderer(this.options, this, presets, model, inputModel); + const result = await renderer.runSelfPreset(); + return RenderOutput.toRenderOutput({result, renderedName: model.name, dependencies: renderer.dependencies}); + } +} diff --git a/src/generators/kotlin/KotlinPreset.ts b/src/generators/kotlin/KotlinPreset.ts new file mode 100644 index 0000000000..e673e6cb94 --- /dev/null +++ b/src/generators/kotlin/KotlinPreset.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { Preset, ClassPreset, EnumPreset } from '../../models'; +import { KotlinOptions } from './KotlinGenerator'; +import { ClassRenderer, KOTLIN_DEFAULT_CLASS_PRESET } from './renderers/ClassRenderer'; +import { EnumRenderer, KOTLIN_DEFAULT_ENUM_PRESET } from './renderers/EnumRenderer'; + +export type ClassPresetType = ClassPreset; +export type EnumPresetType = EnumPreset + +export type KotlinPreset = Preset<{ + class: ClassPresetType; + enum: EnumPresetType; +}>; + +export const KOTLIN_DEFAULT_PRESET: KotlinPreset = { + class: KOTLIN_DEFAULT_CLASS_PRESET, + enum: KOTLIN_DEFAULT_ENUM_PRESET, +}; diff --git a/src/generators/kotlin/KotlinRenderer.ts b/src/generators/kotlin/KotlinRenderer.ts new file mode 100644 index 0000000000..636bab5034 --- /dev/null +++ b/src/generators/kotlin/KotlinRenderer.ts @@ -0,0 +1,29 @@ +import { AbstractRenderer } from '../AbstractRenderer'; +import { KotlinGenerator, KotlinOptions } from './KotlinGenerator'; +import { ConstrainedMetaModel, InputMetaModel, Preset } from '../../models'; +import {FormatHelpers} from '../../helpers'; + +/** + * Common renderer for Kotlin + * + * @extends AbstractRenderer + */ +export abstract class KotlinRenderer extends AbstractRenderer { + constructor( + options: KotlinOptions, + generator: KotlinGenerator, + presets: Array<[Preset, unknown]>, + model: RendererModelType, + inputModel: InputMetaModel, + ) { + super(options, generator, presets, model, inputModel); + } + + renderComments(lines: string | string[]): string { + lines = FormatHelpers.breakLines(lines); + const newLiteral = lines.map(line => ` * ${line}`).join('\n'); + return `/** +${newLiteral} + */`; + } +} diff --git a/src/generators/kotlin/constrainer/EnumConstrainer.ts b/src/generators/kotlin/constrainer/EnumConstrainer.ts new file mode 100644 index 0000000000..74b29b50d7 --- /dev/null +++ b/src/generators/kotlin/constrainer/EnumConstrainer.ts @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { ConstrainedEnumModel, EnumModel } from '../../../models'; +import { NO_NUMBER_START_CHAR, NO_DUPLICATE_ENUM_KEYS, NO_EMPTY_VALUE, NO_RESERVED_KEYWORDS} from '../../../helpers/Constraints'; +import { FormatHelpers, EnumKeyConstraint, EnumValueConstraint} from '../../../helpers'; +import {isInvalidKotlinEnumKey, isReservedKotlinKeyword} from '../Constants'; + +export type ModelEnumKeyConstraints = { + NO_SPECIAL_CHAR: (value: string) => string; + NO_NUMBER_START_CHAR: (value: string) => string; + NO_DUPLICATE_KEYS: (constrainedEnumModel: ConstrainedEnumModel, enumModel: EnumModel, value: string, namingFormatter: (value: string) => string) => string; + NO_EMPTY_VALUE: (value: string) => string; + NAMING_FORMATTER: (value: string) => string; + NO_RESERVED_KEYWORDS: (value: string) => string; +}; + +export const DefaultEnumKeyConstraints: ModelEnumKeyConstraints = { + NO_SPECIAL_CHAR: (value: string) => { + //Exclude '_' because they are allowed as enum keys + return FormatHelpers.replaceSpecialCharacters(value, { exclude: [' ', '_'], separator: '_' }); + }, + NO_NUMBER_START_CHAR, + NO_DUPLICATE_KEYS: NO_DUPLICATE_ENUM_KEYS, + NO_EMPTY_VALUE, + NAMING_FORMATTER: FormatHelpers.toConstantCase, + NO_RESERVED_KEYWORDS: (value: string) => { + return NO_RESERVED_KEYWORDS(value, isInvalidKotlinEnumKey); + } +}; + +export function defaultEnumKeyConstraints(customConstraints?: Partial): EnumKeyConstraint { + const constraints = {...DefaultEnumKeyConstraints, ...customConstraints}; + + return ({enumKey, enumModel, constrainedEnumModel}) => { + let constrainedEnumKey = enumKey; + constrainedEnumKey = constraints.NO_SPECIAL_CHAR(constrainedEnumKey); + constrainedEnumKey = constraints.NO_NUMBER_START_CHAR(constrainedEnumKey); + constrainedEnumKey = constraints.NO_EMPTY_VALUE(constrainedEnumKey); + constrainedEnumKey = constraints.NO_RESERVED_KEYWORDS(constrainedEnumKey); + //If the enum key has been manipulated, lets make sure it don't clash with existing keys + if (constrainedEnumKey !== enumKey) { + constrainedEnumKey = constraints.NO_DUPLICATE_KEYS(constrainedEnumModel, enumModel, constrainedEnumKey, constraints.NAMING_FORMATTER!); + } + constrainedEnumKey = constraints.NAMING_FORMATTER(constrainedEnumKey); + return constrainedEnumKey; + }; +} + +export function defaultEnumValueConstraints(): EnumValueConstraint { + return ({enumValue}) => { + switch (typeof enumValue) { + case 'string': return `"${enumValue}"`; + case 'boolean': + case 'bigint': + case 'number': return enumValue; + case 'object': return `"${JSON.stringify(enumValue).replace(/"/g, '\\"')}"`; + default: return `"${JSON.stringify(enumValue)}"`; + } + }; +} diff --git a/src/generators/kotlin/constrainer/ModelNameConstrainer.ts b/src/generators/kotlin/constrainer/ModelNameConstrainer.ts new file mode 100644 index 0000000000..b79f874f3a --- /dev/null +++ b/src/generators/kotlin/constrainer/ModelNameConstrainer.ts @@ -0,0 +1,41 @@ +import { NO_NUMBER_START_CHAR, NO_EMPTY_VALUE, NO_RESERVED_KEYWORDS} from '../../../helpers/Constraints'; +import { FormatHelpers, ModelNameConstraint } from '../../../helpers'; +import { isReservedKotlinKeyword } from '../Constants'; + +export type ModelNameConstraints = { + NO_SPECIAL_CHAR: (value: string) => string; + NO_NUMBER_START_CHAR: (value: string) => string; + NO_EMPTY_VALUE: (value: string) => string; + NAMING_FORMATTER: (value: string) => string; + NO_RESERVED_KEYWORDS: (value: string) => string; +}; + +export const DefaultModelNameConstraints: ModelNameConstraints = { + NO_SPECIAL_CHAR: (value: string) => { + //Exclude ` ` because it gets formatted by NAMING_FORMATTER + //Exclude '_' because they are allowed + return FormatHelpers.replaceSpecialCharacters(value, { exclude: [' ', '_'], separator: '_' }); + }, + NO_NUMBER_START_CHAR, + NO_EMPTY_VALUE, + NAMING_FORMATTER: (value: string) => { + return FormatHelpers.toPascalCase(value); + }, + NO_RESERVED_KEYWORDS: (value: string) => { + return NO_RESERVED_KEYWORDS(value, isReservedKotlinKeyword); + } +}; + +export function defaultModelNameConstraints(customConstraints?: Partial): ModelNameConstraint { + const constraints = {...DefaultModelNameConstraints, ...customConstraints}; + + return ({modelName}) => { + let constrainedValue = modelName; + constrainedValue = constraints.NO_SPECIAL_CHAR(constrainedValue); + constrainedValue = constraints.NO_NUMBER_START_CHAR(constrainedValue); + constrainedValue = constraints.NO_EMPTY_VALUE(constrainedValue); + constrainedValue = constraints.NO_RESERVED_KEYWORDS(constrainedValue); + constrainedValue = constraints.NAMING_FORMATTER(constrainedValue); + return constrainedValue; + }; +} diff --git a/src/generators/kotlin/constrainer/PropertyKeyConstrainer.ts b/src/generators/kotlin/constrainer/PropertyKeyConstrainer.ts new file mode 100644 index 0000000000..751c19219d --- /dev/null +++ b/src/generators/kotlin/constrainer/PropertyKeyConstrainer.ts @@ -0,0 +1,47 @@ +import { ConstrainedObjectModel, ObjectModel } from '../../../models'; +import { NO_NUMBER_START_CHAR, NO_DUPLICATE_PROPERTIES, NO_EMPTY_VALUE, NO_RESERVED_KEYWORDS} from '../../../helpers/Constraints'; +import { FormatHelpers, PropertyKeyConstraint } from '../../../helpers'; +import { isReservedKotlinKeyword } from '../Constants'; + +export type PropertyKeyConstraintOptions = { + NO_SPECIAL_CHAR: (value: string) => string; + NO_NUMBER_START_CHAR: (value: string) => string; + NO_DUPLICATE_PROPERTIES: (constrainedObjectModel: ConstrainedObjectModel, objectModel: ObjectModel, propertyName: string, namingFormatter: (value: string) => string) => string; + NO_EMPTY_VALUE: (value: string) => string; + NAMING_FORMATTER: (value: string) => string; + NO_RESERVED_KEYWORDS: (value: string) => string; +}; + +export const DefaultPropertyKeyConstraints: PropertyKeyConstraintOptions = { + NO_SPECIAL_CHAR: (value: string) => { + //Exclude ` ` because it gets formatted by NAMING_FORMATTER + //Exclude '_' because they are allowed + return FormatHelpers.replaceSpecialCharacters(value, { exclude: [' ', '_'], separator: '_' }); + }, + NO_NUMBER_START_CHAR, + NO_DUPLICATE_PROPERTIES, + NO_EMPTY_VALUE, + NAMING_FORMATTER: FormatHelpers.toCamelCase, + NO_RESERVED_KEYWORDS: (value: string) => { + return NO_RESERVED_KEYWORDS(value, isReservedKotlinKeyword); + } +}; + +export function defaultPropertyKeyConstraints(customConstraints?: Partial): PropertyKeyConstraint { + const constraints = {...DefaultPropertyKeyConstraints, ...customConstraints}; + + return ({objectPropertyModel, constrainedObjectModel, objectModel}) => { + let constrainedPropertyKey = objectPropertyModel.propertyName; + + constrainedPropertyKey = constraints.NO_SPECIAL_CHAR(constrainedPropertyKey); + constrainedPropertyKey = constraints.NO_NUMBER_START_CHAR(constrainedPropertyKey); + constrainedPropertyKey = constraints.NO_EMPTY_VALUE(constrainedPropertyKey); + constrainedPropertyKey = constraints.NO_RESERVED_KEYWORDS(constrainedPropertyKey); + //If the property name has been manipulated, lets make sure it don't clash with existing properties + if (constrainedPropertyKey !== objectPropertyModel.propertyName) { + constrainedPropertyKey = constraints.NO_DUPLICATE_PROPERTIES(constrainedObjectModel, objectModel, constrainedPropertyKey, constraints.NAMING_FORMATTER); + } + constrainedPropertyKey = constraints.NAMING_FORMATTER(constrainedPropertyKey); + return constrainedPropertyKey; + }; +} diff --git a/src/generators/kotlin/index.ts b/src/generators/kotlin/index.ts new file mode 100644 index 0000000000..71f671d488 --- /dev/null +++ b/src/generators/kotlin/index.ts @@ -0,0 +1,21 @@ +export * from './KotlinGenerator'; +export * from './KotlinFileGenerator'; +export { KOTLIN_DEFAULT_PRESET } from './KotlinPreset'; +export type { KotlinPreset } from './KotlinPreset'; +export * from './presets'; + +export { + defaultEnumKeyConstraints as kotlinDefaultEnumKeyConstraints, + DefaultEnumKeyConstraints as KotlinDefaultEnumKeyConstraints, + defaultEnumValueConstraints as kotlinDefaultEnumValueConstraints +} from './constrainer/EnumConstrainer'; + +export { + DefaultModelNameConstraints as KotlinDefaultModelNameConstraints, + defaultModelNameConstraints as kotlinDefaultModelNameConstraints +} from './constrainer/ModelNameConstrainer'; + +export { + DefaultPropertyKeyConstraints as KotlinDefaultPropertyKeyConstraints, + defaultPropertyKeyConstraints as kotlinDefaultPropertyKeyConstraints +} from './constrainer/PropertyKeyConstrainer'; diff --git a/src/generators/kotlin/presets/DescriptionPreset.ts b/src/generators/kotlin/presets/DescriptionPreset.ts new file mode 100644 index 0000000000..1071656d54 --- /dev/null +++ b/src/generators/kotlin/presets/DescriptionPreset.ts @@ -0,0 +1,25 @@ +import { KotlinPreset } from '../KotlinPreset'; + +/** + * Preset which adds description to rendered model. + * + * @implements {KotlinPreset} + */ +export const KOTLIN_DESCRIPTION_PRESET: KotlinPreset = { + class: { + self({ content }) { + const renderedDesc = 'my description'; + return `${renderedDesc}\n${content}`; + }, + getter({ content }) { + const renderedDesc = 'my description'; + return `${renderedDesc}\n${content}`; + } + }, + enum: { + self({ content }) { + const renderedDesc = 'my description'; + return `${renderedDesc}\n${content}`; + }, + } +}; diff --git a/src/generators/kotlin/presets/index.ts b/src/generators/kotlin/presets/index.ts new file mode 100644 index 0000000000..e74c57b288 --- /dev/null +++ b/src/generators/kotlin/presets/index.ts @@ -0,0 +1 @@ +export * from './DescriptionPreset'; diff --git a/src/generators/kotlin/renderers/ClassRenderer.ts b/src/generators/kotlin/renderers/ClassRenderer.ts new file mode 100644 index 0000000000..cf4083f840 --- /dev/null +++ b/src/generators/kotlin/renderers/ClassRenderer.ts @@ -0,0 +1,89 @@ +import { KotlinRenderer } from '../KotlinRenderer'; +import { ConstrainedDictionaryModel, ConstrainedObjectModel, ConstrainedObjectPropertyModel } from '../../../models'; +import { FormatHelpers } from '../../../helpers'; +import { KotlinOptions } from '../KotlinGenerator'; +import { ClassPresetType } from '../KotlinPreset'; + +/** + * Renderer for Kotlin's `class` type + * + * @extends KotlinRenderer + */ +export class ClassRenderer extends KotlinRenderer { + async defaultSelf(): Promise { + const content = [ + await this.renderProperties(), + await this.runCtorPreset(), + await this.renderAccessors(), + await this.runAdditionalContentPreset(), + ]; + + return `public class ${this.model.name} { +${this.indent(this.renderBlock(content, 2))} +}`; + } + + runCtorPreset(): Promise { + return this.runPreset('ctor'); + } + + /** + * Render all the properties for the class. + */ + async renderProperties(): Promise { + const properties = this.model.properties || {}; + const content: string[] = []; + + for (const property of Object.values(properties)) { + const rendererProperty = await this.runPropertyPreset(property); + content.push(rendererProperty); + } + + return this.renderBlock(content); + } + + runPropertyPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('property', { property }); + } + + /** + * Render all the accessors for the properties + */ + async renderAccessors(): Promise { + const properties = this.model.properties || {}; + const content: string[] = []; + + for (const property of Object.values(properties)) { + const getter = await this.runGetterPreset(property); + const setter = await this.runSetterPreset(property); + content.push(this.renderBlock([getter, setter])); + } + + return this.renderBlock(content, 2); + } + + runGetterPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('getter', { property }); + } + + runSetterPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('setter', { property }); + } +} + +export const KOTLIN_DEFAULT_CLASS_PRESET: ClassPresetType = { + self({ renderer }) { + return renderer.defaultSelf(); + }, + property({ property }) { + return `private ${property.property.type} ${property.propertyName};`; + }, + getter({ property }) { + const getterName = `get${FormatHelpers.toPascalCase(property.propertyName)}`; + return `public ${property.property.type} ${getterName}() { return this.${property.propertyName}; }`; + }, + setter({ property }) { + const setterName = FormatHelpers.toPascalCase(property.propertyName); + return `public void set${setterName}(${property.property.type} ${property.propertyName}) { this.${property.propertyName} = ${property.propertyName}; }`; + } +}; diff --git a/src/generators/kotlin/renderers/EnumRenderer.ts b/src/generators/kotlin/renderers/EnumRenderer.ts new file mode 100644 index 0000000000..02adb0d9a0 --- /dev/null +++ b/src/generators/kotlin/renderers/EnumRenderer.ts @@ -0,0 +1,52 @@ +import { KotlinRenderer } from '../KotlinRenderer'; +import { ConstrainedEnumModel, ConstrainedEnumValueModel} from '../../../models'; +import { EnumPresetType } from '../KotlinPreset'; +import { KotlinOptions } from '../KotlinGenerator'; + +/** + * Renderer for Kotlin's `enum` type + * + * @extends KotlinRenderer + */ +export class EnumRenderer extends KotlinRenderer { + async defaultSelf(valueType: string): Promise { + const content = [ + await this.renderItems(), + await this.runFromValuePreset(), + await this.runAdditionalContentPreset() + ]; + return `enum class ${this.model.name}(val value: ${valueType}) { +${this.indent(this.renderBlock(content, 2))} +}`; + } + + async renderItems(): Promise { + const enums = this.model.values || []; + const items: string[] = []; + + for (const value of enums) { + const renderedItem = await this.runItemPreset(value); + items.push(renderedItem); + } + + const content = items.join(', \n'); + return `${content};`; + } + + runItemPreset(item: ConstrainedEnumValueModel): Promise { + return this.runPreset('item', { item }); + } + + runFromValuePreset(): Promise { + return this.runPreset('fromValue'); + } +} + +export const KOTLIN_DEFAULT_ENUM_PRESET: EnumPresetType = { + self({renderer, model}) { + return renderer.defaultSelf(model.type); + }, + item({ item }) { + return `${item.key}(${item.value})`; + } +}; diff --git a/test/TestUtils/TestRenderers.ts b/test/TestUtils/TestRenderers.ts index bba72bda17..d8c38bab4d 100644 --- a/test/TestUtils/TestRenderers.ts +++ b/test/TestUtils/TestRenderers.ts @@ -8,6 +8,7 @@ import { testOptions, TestGenerator } from './TestGenerator'; import { DartRenderer } from '../../src/generators/dart/DartRenderer'; import { RustRenderer } from '../../src/generators/rust/RustRenderer'; import { PythonRenderer } from '../../src/generators/python/PythonRenderer'; +import { KotlinRenderer } from '../../src/generators/kotlin/KotlinRenderer'; export class TestRenderer extends AbstractRenderer { constructor(presets = []) { @@ -26,3 +27,5 @@ export class MockJavaScriptRenderer extends JavaScriptRenderer { } export class MockDartRenderer extends DartRenderer { } export class MockRustRenderer extends RustRenderer { } export class MockPythonRenderer extends PythonRenderer { } + +export class MockKotlinRenderer extends KotlinRenderer { } diff --git a/test/generators/kotlin/Constants.spec.ts b/test/generators/kotlin/Constants.spec.ts new file mode 100644 index 0000000000..8c3477c443 --- /dev/null +++ b/test/generators/kotlin/Constants.spec.ts @@ -0,0 +1,11 @@ +import { isReservedKotlinKeyword } from '../../../src/generators/kotlin/Constants'; + +describe('Reserved keywords', () => { + it('shoud return true if the word is a reserved keyword', () => { + expect(isReservedKotlinKeyword('as')).toBe(true); + }); + + it('should return false if the word is not a reserved keyword', () => { + expect(isReservedKotlinKeyword('dinosaur')).toBe(false); + }); +}); diff --git a/test/generators/kotlin/KotlinConstrainer.spec.ts b/test/generators/kotlin/KotlinConstrainer.spec.ts new file mode 100644 index 0000000000..4eb208ab17 --- /dev/null +++ b/test/generators/kotlin/KotlinConstrainer.spec.ts @@ -0,0 +1,215 @@ +import { KotlinDefaultTypeMapping } from '../../../src/generators/kotlin/KotlinConstrainer'; +import { KotlinGenerator, KotlinOptions } from '../../../src/generators/kotlin'; +import { ConstrainedAnyModel, ConstrainedArrayModel, ConstrainedBooleanModel, ConstrainedDictionaryModel, ConstrainedEnumModel, ConstrainedEnumValueModel, ConstrainedFloatModel, ConstrainedIntegerModel, ConstrainedObjectModel, ConstrainedReferenceModel, ConstrainedStringModel, ConstrainedTupleModel, ConstrainedTupleValueModel, ConstrainedUnionModel } from '../../../src'; +describe('KotlinConstrainer', () => { + describe('ObjectModel', () => { + test('should render the constrained name as type', () => { + const model = new ConstrainedObjectModel('test', undefined, '', {}); + const type = KotlinDefaultTypeMapping.Object({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual(model.name); + }); + }); + describe('Reference', () => { + test('should render the constrained name as type', () => { + const refModel = new ConstrainedAnyModel('test', undefined, ''); + const model = new ConstrainedReferenceModel('test', undefined, '', refModel); + const type = KotlinDefaultTypeMapping.Reference({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual(model.name); + }); + }); + describe('Any', () => { + test('should render type', () => { + const model = new ConstrainedAnyModel('test', undefined, ''); + const type = KotlinDefaultTypeMapping.Any({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('Any'); + }); + }); + describe('Float', () => { + test('should render type', () => { + const model = new ConstrainedFloatModel('test', undefined, ''); + const type = KotlinDefaultTypeMapping.Float({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('Double'); + }); + test('should render Float when original input has number format', () => { + const model = new ConstrainedFloatModel('test', {format: 'float'}, ''); + const type = KotlinDefaultTypeMapping.Float({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Float'); + }); + }); + describe('Integer', () => { + test('should render type', () => { + const model = new ConstrainedIntegerModel('test', undefined, ''); + const type = KotlinDefaultTypeMapping.Integer({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('Int'); + }); + test('should render Int when original input has integer format', () => { + const model = new ConstrainedIntegerModel('test', {format: 'int32'}, ''); + const type = KotlinDefaultTypeMapping.Integer({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('Int'); + }); + test('should render Long when original input has long format', () => { + const model = new ConstrainedIntegerModel('test', {format: 'long'}, ''); + const type = KotlinDefaultTypeMapping.Integer({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('Long'); + }); + test('should render Long when original input has int64 format', () => { + const model = new ConstrainedIntegerModel('test', {format: 'int64'}, ''); + const type = KotlinDefaultTypeMapping.Integer({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('Long'); + }); + }); + describe('String', () => { + test('should render type', () => { + const model = new ConstrainedStringModel('test', undefined, ''); + const type = KotlinDefaultTypeMapping.String({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('String'); + }); + test('should render LocalDate when original input has date format', () => { + const model = new ConstrainedStringModel('test', {format: 'date'}, ''); + const type = KotlinDefaultTypeMapping.String({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('java.time.LocalDate'); + }); + test('should render OffsetTime when original input has time format', () => { + const model = new ConstrainedStringModel('test', {format: 'time'}, ''); + const type = KotlinDefaultTypeMapping.String({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('java.time.OffsetTime'); + }); + test('should render OffsetDateTime when original input has dateTime format', () => { + const model = new ConstrainedStringModel('test', {format: 'dateTime'}, ''); + const type = KotlinDefaultTypeMapping.String({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('java.time.OffsetDateTime'); + }); + test('should render OffsetDateTime when original input has date-time format', () => { + const model = new ConstrainedStringModel('test', {format: 'date-time'}, ''); + const type = KotlinDefaultTypeMapping.String({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('java.time.OffsetDateTime'); + }); + test('should render byte when original input has binary format', () => { + const model = new ConstrainedStringModel('test', {format: 'binary'}, ''); + const type = KotlinDefaultTypeMapping.String({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('ByteArray'); + }); + }); + + describe('Boolean', () => { + test('should render type', () => { + const model = new ConstrainedBooleanModel('test', undefined, ''); + const type = KotlinDefaultTypeMapping.Boolean({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('Boolean'); + }); + }); + + describe('Tuple', () => { + test('should render type', () => { + const stringModel = new ConstrainedStringModel('test', undefined, 'String'); + const tupleValueModel = new ConstrainedTupleValueModel(0, stringModel); + const model = new ConstrainedTupleModel('test', undefined, '', [tupleValueModel]); + const type = KotlinDefaultTypeMapping.Tuple({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('List'); + }); + test('should render multiple tuple types', () => { + const stringModel = new ConstrainedStringModel('test', undefined, 'String'); + const tupleValueModel0 = new ConstrainedTupleValueModel(0, stringModel); + const tupleValueModel1 = new ConstrainedTupleValueModel(1, stringModel); + const model = new ConstrainedTupleModel('test', undefined, '', [tupleValueModel0, tupleValueModel1]); + const type = KotlinDefaultTypeMapping.Tuple({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('List'); + }); + }); + + describe('Array', () => { + test('should render type', () => { + const arrayModel = new ConstrainedStringModel('test', undefined, 'String'); + const model = new ConstrainedArrayModel('test', undefined, '', arrayModel); + const options: KotlinOptions = {...KotlinGenerator.defaultOptions, collectionType: 'Array'}; + const type = KotlinDefaultTypeMapping.Array({constrainedModel: model, options}); + expect(type).toEqual('String[]'); + }); + test('should render array as a list', () => { + const arrayModel = new ConstrainedStringModel('test', undefined, 'String'); + const model = new ConstrainedArrayModel('test', undefined, '', arrayModel); + const options: KotlinOptions = {...KotlinGenerator.defaultOptions, collectionType: 'List'}; + const type = KotlinDefaultTypeMapping.Array({constrainedModel: model, options}); + expect(type).toEqual('List'); + }); + }); + + describe('Enum', () => { + test('should render string enum values as String type', () => { + const enumValue = new ConstrainedEnumValueModel('test', 'string type'); + const model = new ConstrainedEnumModel('test', undefined, '', [enumValue]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('String'); + }); + test('should render boolean enum values as boolean type', () => { + const enumValue = new ConstrainedEnumValueModel('test', true); + const model = new ConstrainedEnumModel('test', undefined, '', [enumValue]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Boolean'); + }); + test('should render generic number enum value with format ', () => { + const enumValue = new ConstrainedEnumValueModel('test', 123); + const model = new ConstrainedEnumModel('test', undefined, '', [enumValue]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Int'); + }); + test('should render generic number enum value with float format as float type', () => { + const enumValue = new ConstrainedEnumValueModel('test', 12.0); + const model = new ConstrainedEnumModel('test', {format: 'float'}, '', [enumValue]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Float'); + }); + test('should render generic number enum value with double format as double type', () => { + const enumValue = new ConstrainedEnumValueModel('test', 12.0); + const model = new ConstrainedEnumModel('test', {format: 'double'}, '', [enumValue]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Double'); + }); + test('should render object enum value as generic Object', () => { + const enumValue = new ConstrainedEnumValueModel('test', {}); + const model = new ConstrainedEnumModel('test', undefined, '', [enumValue]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Any'); + }); + test('should render multiple value types as generic Object', () => { + const enumValue2 = new ConstrainedEnumValueModel('test', true); + const enumValue1 = new ConstrainedEnumValueModel('test', 'string type'); + const model = new ConstrainedEnumModel('test', undefined, '', [enumValue1, enumValue2]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Any'); + }); + test('should render double and integer as double type', () => { + const enumValue2 = new ConstrainedEnumValueModel('test', 123); + const enumValue1 = new ConstrainedEnumValueModel('test', 123.12); + const model = new ConstrainedEnumModel('test', undefined, '', [enumValue1, enumValue2]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Double'); + }); + test('should render int and long as long type', () => { + const enumValue2 = new ConstrainedEnumValueModel('test', 123); + const enumValue1 = new ConstrainedEnumValueModel('test', 123); + const model = new ConstrainedEnumModel('test', {format: 'long'}, '', [enumValue1, enumValue2]); + const type = KotlinDefaultTypeMapping.Enum({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Long'); + }); + }); + + describe('Union', () => { + test('should render type', () => { + const unionModel = new ConstrainedStringModel('test', undefined, 'str'); + const model = new ConstrainedUnionModel('test', undefined, '', [unionModel]); + const type = KotlinDefaultTypeMapping.Union({constrainedModel: model, options: KotlinGenerator.defaultOptions}); + expect(type).toEqual('Any'); + }); + }); + + describe('Dictionary', () => { + test('should render type', () => { + const keyModel = new ConstrainedStringModel('test', undefined, 'String'); + const valueModel = new ConstrainedStringModel('test', undefined, 'String'); + const model = new ConstrainedDictionaryModel('test', undefined, '', keyModel, valueModel); + const type = KotlinDefaultTypeMapping.Dictionary({ constrainedModel: model, options: KotlinGenerator.defaultOptions }); + expect(type).toEqual('Map'); + }); + }); +}); diff --git a/test/generators/kotlin/KotlinGenerator.spec.ts b/test/generators/kotlin/KotlinGenerator.spec.ts new file mode 100644 index 0000000000..0f17ca9422 --- /dev/null +++ b/test/generators/kotlin/KotlinGenerator.spec.ts @@ -0,0 +1,132 @@ +import { KotlinGenerator } from '../../../src/generators/kotlin'; + +describe('KotlinGenerator', () => { + let generator: KotlinGenerator; + beforeEach(() => { + generator = new KotlinGenerator(); + }); + + describe('Enum', () => { + test('should render `enum` with mixed types (union type)', async () => { + const doc = { + $id: 'Things', + enum: ['Texas', 1, '1', false, { test: 'test' }], + }; + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + test('should work custom preset for `enum` type', async () => { + const doc = { + $id: 'CustomEnum', + type: 'string', + enum: ['Texas', 'Alabama', 'California'], + }; + + generator = new KotlinGenerator({ + presets: [ + { + enum: { + self({ content }) { + return content; + }, + } + } + ] + }); + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + test('should render enums with translated special characters', async () => { + const doc = { + $id: 'States', + enum: ['test+', '$test', 'test-', 'test?!', '*test'] + }; + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + }); + describe('Class', () => { + test('should not render reserved keyword', async () => { + const doc = { + $id: 'Address', + type: 'object', + properties: { + enum: { type: 'string' }, + reservedEnum: { type: 'string' } + }, + additionalProperties: false + }; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + test('should render `class` type', async () => { + const doc = { + $id: 'Address', + type: 'object', + properties: { + street_name: { type: 'string' }, + city: { type: 'string', description: 'City description' }, + state: { type: 'string' }, + house_number: { type: 'number' }, + marriage: { type: 'boolean', description: 'Status if marriage live in given house' }, + members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }, + array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] }, + }, + patternProperties: { + '^S(.?*)test&': { + type: 'string' + } + }, + required: ['street_name', 'city', 'state', 'house_number', 'array_type'], + }; + const expectedDependencies: string[] = []; + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + expect(models[0].dependencies).toEqual(expectedDependencies); + }); + + test('should work with custom preset for `class` type', async () => { + const doc = { + $id: 'CustomClass', + type: 'object', + properties: { + property: { type: 'string' }, + } + }; + generator = new KotlinGenerator({ presets: [ + { + class: { + property({ content }) { + const annotation = 'test1'; + return `${annotation}\n${content}`; + }, + getter({ content }) { + const annotation = 'test2'; + return `${annotation}\n${content}`; + }, + setter({ content }) { + const annotation = 'test3'; + return `${annotation}\n${content}`; + }, + } + } + ] }); + const expectedDependencies: string[] = []; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + expect(models[0].dependencies).toEqual(expectedDependencies); + }); + }); +}); diff --git a/test/generators/kotlin/KotlinRenderer.spec.ts b/test/generators/kotlin/KotlinRenderer.spec.ts new file mode 100644 index 0000000000..55ef433aba --- /dev/null +++ b/test/generators/kotlin/KotlinRenderer.spec.ts @@ -0,0 +1,19 @@ +import { KotlinGenerator } from '../../../src/generators/kotlin'; +import { KotlinRenderer } from '../../../src/generators/kotlin/KotlinRenderer'; +import { ConstrainedObjectModel, InputMetaModel } from '../../../src/models'; +import { MockKotlinRenderer } from '../../TestUtils/TestRenderers'; + +describe('KotlinRenderer', () => { + let renderer: KotlinRenderer; + beforeEach(() => { + renderer = new MockKotlinRenderer(KotlinGenerator.defaultOptions, new KotlinGenerator(), [], new ConstrainedObjectModel('', undefined, '', {}), new InputMetaModel()); + }); + + describe('renderComments()', () => { + test('Should be able to render comments', () => { + expect(renderer.renderComments('someComment')).toEqual(`/** + * someComment + */`); + }); + }); +}); diff --git a/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap b/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap new file mode 100644 index 0000000000..44149ca720 --- /dev/null +++ b/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KotlinGenerator Class should not render reserved keyword 1`] = ` +"public class Address { + private enum; + private reservedEnum; + + public getEnum() { return this.enum; } + public void setEnum( enum) { this.enum = enum; } + + public getReservedEnum() { return this.reservedEnum; } + public void setReservedEnum( reservedEnum) { this.reservedEnum = reservedEnum; } +}" +`; + +exports[`KotlinGenerator Class should render \`class\` type 1`] = ` +"public class Address { + private streetName; + private city; + private state; + private houseNumber; + private marriage; + private members; + private arrayType; + private additionalProperties; + + public getStreetName() { return this.streetName; } + public void setStreetName( streetName) { this.streetName = streetName; } + + public getCity() { return this.city; } + public void setCity( city) { this.city = city; } + + public getState() { return this.state; } + public void setState( state) { this.state = state; } + + public getHouseNumber() { return this.houseNumber; } + public void setHouseNumber( houseNumber) { this.houseNumber = houseNumber; } + + public getMarriage() { return this.marriage; } + public void setMarriage( marriage) { this.marriage = marriage; } + + public getMembers() { return this.members; } + public void setMembers( members) { this.members = members; } + + public getArrayType() { return this.arrayType; } + public void setArrayType( arrayType) { this.arrayType = arrayType; } + + public getAdditionalProperties() { return this.additionalProperties; } + public void setAdditionalProperties( additionalProperties) { this.additionalProperties = additionalProperties; } +}" +`; + +exports[`KotlinGenerator Class should work with custom preset for \`class\` type 1`] = ` +"public class CustomClass { + test1 + private property; + test1 + private additionalProperties; + + test2 + public getProperty() { return this.property; } + test3 + public void setProperty( property) { this.property = property; } + + test2 + public getAdditionalProperties() { return this.additionalProperties; } + test3 + public void setAdditionalProperties( additionalProperties) { this.additionalProperties = additionalProperties; } +}" +`; + +exports[`KotlinGenerator Enum should render \`enum\` with mixed types (union type) 1`] = ` +"public enum Things { + TEXAS(\\"Texas\\"), NUMBER_1(1), RESERVED_NUMBER_1(\\"1\\"), FALSE(\\"false\\"), CURLYLEFT_QUOTATION_TEST_QUOTATION_COLON_QUOTATION_TEST_QUOTATION_CURLYRIGHT(\\"{\\\\\\"test\\\\\\":\\\\\\"test\\\\\\"}\\"); + + private Object value; + + Things(Object value) { + this.value = value; + } + + @JsonValue + public Object getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static Things fromValue(Object value) { + for (Things e : Things.values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException(\\"Unexpected value '\\" + value + \\"'\\"); + } +}" +`; + +exports[`KotlinGenerator Enum should render enums with translated special characters 1`] = ` +"public enum States { + TEST_PLUS(\\"test+\\"), DOLLAR_TEST(\\"$test\\"), TEST_MINUS(\\"test-\\"), TEST_QUESTION_EXCLAMATION(\\"test?!\\"), ASTERISK_TEST(\\"*test\\"); + + private Object value; + + States(Object value) { + this.value = value; + } + + @JsonValue + public Object getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static States fromValue(Object value) { + for (States e : States.values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException(\\"Unexpected value '\\" + value + \\"'\\"); + } +}" +`; + +exports[`KotlinGenerator Enum should work custom preset for \`enum\` type 1`] = ` +"public enum CustomEnum { + TEXAS(\\"Texas\\"), ALABAMA(\\"Alabama\\"), CALIFORNIA(\\"California\\"); + + private Object value; + + CustomEnum(Object value) { + this.value = value; + } + + @JsonValue + public Object getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static CustomEnum fromValue(Object value) { + for (CustomEnum e : CustomEnum.values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException(\\"Unexpected value '\\" + value + \\"'\\"); + } +}" +`; diff --git a/test/generators/kotlin/presets/DescriptionPreset.spec.ts b/test/generators/kotlin/presets/DescriptionPreset.spec.ts new file mode 100644 index 0000000000..8a44845b26 --- /dev/null +++ b/test/generators/kotlin/presets/DescriptionPreset.spec.ts @@ -0,0 +1,40 @@ +import { KotlinGenerator, KOTLIN_DESCRIPTION_PRESET } from '../../../../src/generators/kotlin'; + +describe('KOTLIN_DESCRIPTION_PRESET', () => { + let generator: KotlinGenerator; + beforeEach(() => { + generator = new KotlinGenerator({ presets: [KOTLIN_DESCRIPTION_PRESET] }); + }); + + test('should render description and examples for class', async () => { + const doc = { + $id: 'Clazz', + type: 'object', + description: 'Description for class', + examples: [{ prop: 'value' }], + properties: { + prop: { type: 'string', description: 'Description for prop', examples: ['exampleValue'] }, + }, + }; + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + test('should render description and examples for enum', async () => { + const doc = { + $id: 'Enum', + type: 'string', + description: 'Description for enum', + examples: ['value'], + enum: [ + 'on', + 'off', + ] + }; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); +}); diff --git a/test/generators/kotlin/presets/__snapshots__/DescriptionPreset.spec.ts.snap b/test/generators/kotlin/presets/__snapshots__/DescriptionPreset.spec.ts.snap new file mode 100644 index 0000000000..ea26265097 --- /dev/null +++ b/test/generators/kotlin/presets/__snapshots__/DescriptionPreset.spec.ts.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KOTLIN_DESCRIPTION_PRESET should render description and examples for class 1`] = ` +"my description +public class Clazz { + private String prop; + private Map additionalProperties; + + my description + public String getProp() { return this.prop; } + public void setProp(String prop) { this.prop = prop; } + + my description + public Map getAdditionalProperties() { return this.additionalProperties; } + public void setAdditionalProperties(Map additionalProperties) { this.additionalProperties = additionalProperties; } +}" +`; + +exports[`KOTLIN_DESCRIPTION_PRESET should render description and examples for enum 1`] = ` +"my description +enum class ReservedEnum(val value: String) { + ON(\\"on\\" as String), + OFF(\\"off\\" as String); + + companion object { + fun fromValue(value: String) = + ReservedEnum.values().firstOrNull { it.value.contentEquals(value, ignoringCase = true ) } + ?: throw IllegalArgumentException(\\"Unexpected value '$value'\\") + } +}" +`; From db5e83c97ec863d5061240756ebacb199cfcc176 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 18:20:01 +0100 Subject: [PATCH 03/45] wip: class generator seems to work --- examples/generate-kotlin-enums/README.md | 2 +- examples/generate-kotlin-enums/index.spec.ts | 2 +- examples/generate-kotlin-enums/index.ts | 2 +- examples/generate-kotlin-models/README.md | 17 ++++++ examples/generate-kotlin-models/index.spec.ts | 13 +++++ examples/generate-kotlin-models/index.ts | 41 +++++++++++++++ .../generate-kotlin-models/package-lock.json | 10 ++++ examples/generate-kotlin-models/package.json | 12 +++++ .../kotlin/renderers/ClassRenderer.ts | 52 ++----------------- 9 files changed, 101 insertions(+), 50 deletions(-) create mode 100644 examples/generate-kotlin-models/README.md create mode 100644 examples/generate-kotlin-models/index.spec.ts create mode 100644 examples/generate-kotlin-models/index.ts create mode 100644 examples/generate-kotlin-models/package-lock.json create mode 100644 examples/generate-kotlin-models/package.json diff --git a/examples/generate-kotlin-enums/README.md b/examples/generate-kotlin-enums/README.md index d9b12e1b38..d1c7c70214 100644 --- a/examples/generate-kotlin-enums/README.md +++ b/examples/generate-kotlin-enums/README.md @@ -1,4 +1,4 @@ -# Java Data Models +# Kotlin Enums A basic example of how to use Modelina and output a Kotlin enum. diff --git a/examples/generate-kotlin-enums/index.spec.ts b/examples/generate-kotlin-enums/index.spec.ts index 3e6e222d60..2915f2356b 100644 --- a/examples/generate-kotlin-enums/index.spec.ts +++ b/examples/generate-kotlin-enums/index.spec.ts @@ -1,7 +1,7 @@ const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; }); import {generate} from './index'; -describe('Should be able to render Java Models', () => { +describe('Should be able to render Kotlin Enums', () => { afterAll(() => { jest.restoreAllMocks(); }); diff --git a/examples/generate-kotlin-enums/index.ts b/examples/generate-kotlin-enums/index.ts index a0db6cafb4..86e351e577 100644 --- a/examples/generate-kotlin-enums/index.ts +++ b/examples/generate-kotlin-enums/index.ts @@ -1,4 +1,4 @@ -import {JavaGenerator, KotlinGenerator} from '../../src'; +import { KotlinGenerator} from '../../src'; const generator = new KotlinGenerator(); const jsonSchemaDraft7 = { diff --git a/examples/generate-kotlin-models/README.md b/examples/generate-kotlin-models/README.md new file mode 100644 index 0000000000..20ca57b90d --- /dev/null +++ b/examples/generate-kotlin-models/README.md @@ -0,0 +1,17 @@ +# Kotlin Data Models + +A basic example of how to use Modelina and output a Kotlin data model. + +## How to run this example + +Run this example using: + +```sh +npm i && npm run start +``` + +If you are on Windows, use the `start:windows` script instead: + +```sh +npm i && npm run start:windows +``` diff --git a/examples/generate-kotlin-models/index.spec.ts b/examples/generate-kotlin-models/index.spec.ts new file mode 100644 index 0000000000..8bbd5c352b --- /dev/null +++ b/examples/generate-kotlin-models/index.spec.ts @@ -0,0 +1,13 @@ +const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; }); +import {generate} from './index'; + +describe('Should be able to render Kotlin Models', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + test('and should log expected output to console', async () => { + await generate(); + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + }); +}); diff --git a/examples/generate-kotlin-models/index.ts b/examples/generate-kotlin-models/index.ts new file mode 100644 index 0000000000..87f6529e9d --- /dev/null +++ b/examples/generate-kotlin-models/index.ts @@ -0,0 +1,41 @@ +import { KotlinGenerator } from '../../src'; + +const generator = new KotlinGenerator(); +const jsonSchemaDraft7 = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + additionalProperties: false, + properties: { + email: { + type: 'string', + format: 'email' + }, + cache: { + type: 'integer' + }, + website: { + type: 'object', + additionalProperties: false, + properties: { + domain: { + type: 'string', + format: 'url' + }, + protocol: { + type: 'string', + enum: ['HTTP', 'HTTPS'], + } + } + } + } +}; + +export async function generate() : Promise { + const models = await generator.generate(jsonSchemaDraft7); + for (const model of models) { + console.log(model.result); + } +} +if (require.main === module) { + generate(); +} diff --git a/examples/generate-kotlin-models/package-lock.json b/examples/generate-kotlin-models/package-lock.json new file mode 100644 index 0000000000..eb514aba77 --- /dev/null +++ b/examples/generate-kotlin-models/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "generate-kotlin-models", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/generate-kotlin-models/package.json b/examples/generate-kotlin-models/package.json new file mode 100644 index 0000000000..d76aefdcfd --- /dev/null +++ b/examples/generate-kotlin-models/package.json @@ -0,0 +1,12 @@ +{ + "config": { + "example_name": "generate-kotlin-models" + }, + "scripts": { + "install": "cd ../.. && npm i", + "start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts", + "start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts", + "test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts", + "test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts" + } +} diff --git a/src/generators/kotlin/renderers/ClassRenderer.ts b/src/generators/kotlin/renderers/ClassRenderer.ts index cf4083f840..6e44002a2c 100644 --- a/src/generators/kotlin/renderers/ClassRenderer.ts +++ b/src/generators/kotlin/renderers/ClassRenderer.ts @@ -1,35 +1,25 @@ import { KotlinRenderer } from '../KotlinRenderer'; -import { ConstrainedDictionaryModel, ConstrainedObjectModel, ConstrainedObjectPropertyModel } from '../../../models'; -import { FormatHelpers } from '../../../helpers'; +import { ConstrainedObjectModel, ConstrainedObjectPropertyModel } from '../../../models'; import { KotlinOptions } from '../KotlinGenerator'; import { ClassPresetType } from '../KotlinPreset'; /** * Renderer for Kotlin's `class` type - * + * * @extends KotlinRenderer */ export class ClassRenderer extends KotlinRenderer { async defaultSelf(): Promise { const content = [ await this.renderProperties(), - await this.runCtorPreset(), - await this.renderAccessors(), await this.runAdditionalContentPreset(), ]; - return `public class ${this.model.name} { + return `data class ${this.model.name}( ${this.indent(this.renderBlock(content, 2))} -}`; +)`; } - runCtorPreset(): Promise { - return this.runPreset('ctor'); - } - - /** - * Render all the properties for the class. - */ async renderProperties(): Promise { const properties = this.model.properties || {}; const content: string[] = []; @@ -45,30 +35,6 @@ ${this.indent(this.renderBlock(content, 2))} runPropertyPreset(property: ConstrainedObjectPropertyModel): Promise { return this.runPreset('property', { property }); } - - /** - * Render all the accessors for the properties - */ - async renderAccessors(): Promise { - const properties = this.model.properties || {}; - const content: string[] = []; - - for (const property of Object.values(properties)) { - const getter = await this.runGetterPreset(property); - const setter = await this.runSetterPreset(property); - content.push(this.renderBlock([getter, setter])); - } - - return this.renderBlock(content, 2); - } - - runGetterPreset(property: ConstrainedObjectPropertyModel): Promise { - return this.runPreset('getter', { property }); - } - - runSetterPreset(property: ConstrainedObjectPropertyModel): Promise { - return this.runPreset('setter', { property }); - } } export const KOTLIN_DEFAULT_CLASS_PRESET: ClassPresetType = { @@ -76,14 +42,6 @@ export const KOTLIN_DEFAULT_CLASS_PRESET: ClassPresetType = { return renderer.defaultSelf(); }, property({ property }) { - return `private ${property.property.type} ${property.propertyName};`; - }, - getter({ property }) { - const getterName = `get${FormatHelpers.toPascalCase(property.propertyName)}`; - return `public ${property.property.type} ${getterName}() { return this.${property.propertyName}; }`; - }, - setter({ property }) { - const setterName = FormatHelpers.toPascalCase(property.propertyName); - return `public void set${setterName}(${property.property.type} ${property.propertyName}) { this.${property.propertyName} = ${property.propertyName}; }`; + return `val ${property.propertyName}: ${property.property.type},`; } }; From 6f8bcf9402b81edf54fffc70c5155871859f3c5e Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 19:49:02 +0100 Subject: [PATCH 04/45] wip: description preset looks fine --- examples/kotlin-generate-kdoc/README.md | 17 ++++++ examples/kotlin-generate-kdoc/index.spec.ts | 13 ++++ examples/kotlin-generate-kdoc/index.ts | 27 +++++++++ .../kotlin-generate-kdoc/package-lock.json | 10 ++++ examples/kotlin-generate-kdoc/package.json | 10 ++++ .../kotlin/presets/DescriptionPreset.ts | 60 +++++++++++++++---- 6 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 examples/kotlin-generate-kdoc/README.md create mode 100644 examples/kotlin-generate-kdoc/index.spec.ts create mode 100644 examples/kotlin-generate-kdoc/index.ts create mode 100644 examples/kotlin-generate-kdoc/package-lock.json create mode 100644 examples/kotlin-generate-kdoc/package.json diff --git a/examples/kotlin-generate-kdoc/README.md b/examples/kotlin-generate-kdoc/README.md new file mode 100644 index 0000000000..56e3fcce84 --- /dev/null +++ b/examples/kotlin-generate-kdoc/README.md @@ -0,0 +1,17 @@ +# Kotlin KDoc + +A basic example of how to generate Kotlin models by including KDoc in description and examples. + +## How to run this example + +Run this example using: + +```sh +npm i && npm run start +``` + +If you are on Windows, use the `start:windows` script instead: + +```sh +npm i && npm run start:windows +``` diff --git a/examples/kotlin-generate-kdoc/index.spec.ts b/examples/kotlin-generate-kdoc/index.spec.ts new file mode 100644 index 0000000000..5792a0a381 --- /dev/null +++ b/examples/kotlin-generate-kdoc/index.spec.ts @@ -0,0 +1,13 @@ +const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; }); +import {generate} from './index'; + +describe('Should be able to generate JavaDocs', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + test('and should log expected output to console', async () => { + await generate(); + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + }); +}); diff --git a/examples/kotlin-generate-kdoc/index.ts b/examples/kotlin-generate-kdoc/index.ts new file mode 100644 index 0000000000..0a51d998da --- /dev/null +++ b/examples/kotlin-generate-kdoc/index.ts @@ -0,0 +1,27 @@ +import { KotlinGenerator, KOTLIN_DESCRIPTION_PRESET } from '../../src'; + +const generator = new KotlinGenerator({ + presets: [KOTLIN_DESCRIPTION_PRESET] +}); +const jsonSchemaDraft7 = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'KDoc', + type: 'object', + description: 'Description for class', + examples: [{ prop: 'value' }, { prop: 'test' }], + properties: { + prop: { type: 'string', description: 'Description for prop', examples: ['exampleValue'] }, + enum: { type: 'string', description: 'Description for enum', enum: ['A', 'B', 'C'] }, + nodesc: { type: 'string' } + }, +}; + +export async function generate() : Promise { + const models = await generator.generate(jsonSchemaDraft7); + for (const model of models) { + console.log(model.result); + } +} +if (require.main === module) { + generate(); +} diff --git a/examples/kotlin-generate-kdoc/package-lock.json b/examples/kotlin-generate-kdoc/package-lock.json new file mode 100644 index 0000000000..c17a948236 --- /dev/null +++ b/examples/kotlin-generate-kdoc/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "kotlin-generate-kdoc", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/kotlin-generate-kdoc/package.json b/examples/kotlin-generate-kdoc/package.json new file mode 100644 index 0000000000..b6dc26945d --- /dev/null +++ b/examples/kotlin-generate-kdoc/package.json @@ -0,0 +1,10 @@ +{ + "config" : { "example_name" : "kotlin-generate-kdoc" }, + "scripts": { + "install": "cd ../.. && npm i", + "start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts", + "start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts", + "test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts", + "test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts" + } +} diff --git a/src/generators/kotlin/presets/DescriptionPreset.ts b/src/generators/kotlin/presets/DescriptionPreset.ts index 1071656d54..a8fccc2e5e 100644 --- a/src/generators/kotlin/presets/DescriptionPreset.ts +++ b/src/generators/kotlin/presets/DescriptionPreset.ts @@ -1,25 +1,61 @@ +import { KotlinRenderer } from '../KotlinRenderer'; import { KotlinPreset } from '../KotlinPreset'; +import { FormatHelpers } from '../../../helpers'; +import {ConstrainedEnumModel, ConstrainedObjectModel} from '../../../models'; +function renderDescription({ renderer, content, item }: { + renderer: KotlinRenderer, + content: string, + item: (ConstrainedObjectModel | ConstrainedEnumModel) +}): string { + const desc = item.originalInput['description']; + + if (!desc) { + return content; + } + + let comment = `${desc}`; + + if (item instanceof ConstrainedObjectModel) { + const obj = item; + + const properties = Object.keys(obj.properties) + .map(key => obj.properties[key]) + .map(model => { + const property = `@property ${model.propertyName}`; + const desc = model.property.originalInput['description']; + + return desc !== undefined ? `${property} ${desc}` : property; + }) + .join('\n'); + + comment += `\n\n${properties}`; + } + + const examples = Array.isArray(item.originalInput['examples']) + ? `Examples: \n${FormatHelpers.renderJSONExamples(item.originalInput['examples'])}` + : null; + + if (examples !== null) { + comment += `\n\n${examples}`; + } + + return `${renderer.renderComments(comment)}\n${content}`; +} /** - * Preset which adds description to rendered model. - * + * Preset which adds description to rendered model. + * * @implements {KotlinPreset} */ export const KOTLIN_DESCRIPTION_PRESET: KotlinPreset = { class: { - self({ content }) { - const renderedDesc = 'my description'; - return `${renderedDesc}\n${content}`; - }, - getter({ content }) { - const renderedDesc = 'my description'; - return `${renderedDesc}\n${content}`; + self({ renderer, model, content }) { + return renderDescription({ renderer, content, item: model}); } }, enum: { - self({ content }) { - const renderedDesc = 'my description'; - return `${renderedDesc}\n${content}`; + self({ renderer, model, content }) { + return renderDescription({ renderer, content, item: model }); }, } }; From f9c6ee8c71c5d990a6909f591159bdd715046eaf Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 20:52:56 +0100 Subject: [PATCH 05/45] wip: fix constrainer test --- test/generators/kotlin/KotlinConstrainer.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/generators/kotlin/KotlinConstrainer.spec.ts b/test/generators/kotlin/KotlinConstrainer.spec.ts index 4eb208ab17..1722b3b43b 100644 --- a/test/generators/kotlin/KotlinConstrainer.spec.ts +++ b/test/generators/kotlin/KotlinConstrainer.spec.ts @@ -123,7 +123,7 @@ describe('KotlinConstrainer', () => { const model = new ConstrainedArrayModel('test', undefined, '', arrayModel); const options: KotlinOptions = {...KotlinGenerator.defaultOptions, collectionType: 'Array'}; const type = KotlinDefaultTypeMapping.Array({constrainedModel: model, options}); - expect(type).toEqual('String[]'); + expect(type).toEqual('Array'); }); test('should render array as a list', () => { const arrayModel = new ConstrainedStringModel('test', undefined, 'String'); From 272356d7533f12f71bec43271d94d6f245983b47 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 21:03:48 +0100 Subject: [PATCH 06/45] wip: test enumconstrainer (aka copy from java impl) --- .../constrainer/EnumConstrainer.spec.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/generators/kotlin/constrainer/EnumConstrainer.spec.ts diff --git a/test/generators/kotlin/constrainer/EnumConstrainer.spec.ts b/test/generators/kotlin/constrainer/EnumConstrainer.spec.ts new file mode 100644 index 0000000000..3a915118ee --- /dev/null +++ b/test/generators/kotlin/constrainer/EnumConstrainer.spec.ts @@ -0,0 +1,90 @@ +import { KotlinDefaultConstraints } from '../../../../src/generators/kotlin/KotlinConstrainer'; +import { EnumModel } from '../../../../src'; +import { ConstrainedEnumModel, ConstrainedEnumValueModel } from '../../../../src'; +import { defaultEnumKeyConstraints, ModelEnumKeyConstraints, DefaultEnumKeyConstraints } from '../../../../src/generators/kotlin/constrainer/EnumConstrainer'; + +describe('EnumConstrainer', () => { + const enumModel = new EnumModel('test', undefined, []); + const constrainedEnumModel = new ConstrainedEnumModel('test', undefined, '', []); + + describe('enum keys', () => { + test('should never render special chars', () => { + const constrainedKey = KotlinDefaultConstraints.enumKey({enumModel, constrainedEnumModel, enumKey: '%'}); + expect(constrainedKey).toEqual('PERCENT'); + }); + test('should not render number as start char', () => { + const constrainedKey = KotlinDefaultConstraints.enumKey({enumModel, constrainedEnumModel, enumKey: '1'}); + expect(constrainedKey).toEqual('NUMBER_1'); + }); + test('should not contain duplicate keys', () => { + const existingConstrainedEnumValueModel = new ConstrainedEnumValueModel('EMPTY', 'return'); + const constrainedEnumModel = new ConstrainedEnumModel('test', undefined, '', [existingConstrainedEnumValueModel]); + const constrainedKey = KotlinDefaultConstraints.enumKey({enumModel, constrainedEnumModel, enumKey: ''}); + expect(constrainedKey).toEqual('RESERVED_EMPTY'); + }); + test('should never contain empty keys', () => { + const constrainedKey = KotlinDefaultConstraints.enumKey({enumModel, constrainedEnumModel, enumKey: ''}); + expect(constrainedKey).toEqual('EMPTY'); + }); + test('should use constant naming format', () => { + const constrainedKey = KotlinDefaultConstraints.enumKey({enumModel, constrainedEnumModel, enumKey: 'some weird_value!"#2'}); + expect(constrainedKey).toEqual('SOME_WEIRD_VALUE_EXCLAMATION_QUOTATION_HASH_2'); + }); + }); + describe('enum values', () => { + test('should render string values', () => { + const constrainedValue = KotlinDefaultConstraints.enumValue({enumModel, constrainedEnumModel, enumValue: 'string value'}); + expect(constrainedValue).toEqual('"string value"'); + }); + test('should render boolean values', () => { + const constrainedValue = KotlinDefaultConstraints.enumValue({enumModel, constrainedEnumModel, enumValue: true}); + expect(constrainedValue).toEqual(true); + }); + test('should render numbers', () => { + const constrainedValue = KotlinDefaultConstraints.enumValue({enumModel, constrainedEnumModel, enumValue: 123}); + expect(constrainedValue).toEqual(123); + }); + test('should render object', () => { + const constrainedValue = KotlinDefaultConstraints.enumValue({enumModel, constrainedEnumModel, enumValue: {test: 'test'}}); + expect(constrainedValue).toEqual('"{\\"test\\":\\"test\\"}"'); + }); + test('should render unknown value', () => { + const constrainedValue = KotlinDefaultConstraints.enumValue({enumModel, constrainedEnumModel, enumValue: undefined}); + expect(constrainedValue).toEqual('"undefined"'); + }); + }); + describe('custom constraints', () => { + test('should be able to overwrite all hooks for enum key', () => { + const mockedConstraintCallbacks: Partial = { + NAMING_FORMATTER: jest.fn().mockReturnValue(''), + NO_SPECIAL_CHAR: jest.fn().mockReturnValue(''), + NO_NUMBER_START_CHAR: jest.fn().mockReturnValue(''), + NO_EMPTY_VALUE: jest.fn().mockReturnValue(''), + NO_RESERVED_KEYWORDS: jest.fn().mockReturnValue('') + }; + const constrainFunction = defaultEnumKeyConstraints(mockedConstraintCallbacks); + constrainFunction({enumModel, constrainedEnumModel, enumKey: ''}); + //Expect all callbacks to be called + for (const jestMockCallback of Object.values(mockedConstraintCallbacks)) { + expect(jestMockCallback).toHaveBeenCalled(); + } + }); + test('should be able to overwrite one hooks for enum key', () => { + //All but NAMING_FORMATTER, as we customize that + const spies = [ + jest.spyOn(DefaultEnumKeyConstraints, 'NO_SPECIAL_CHAR'), + jest.spyOn(DefaultEnumKeyConstraints, 'NO_NUMBER_START_CHAR'), + jest.spyOn(DefaultEnumKeyConstraints, 'NO_DUPLICATE_KEYS'), + jest.spyOn(DefaultEnumKeyConstraints, 'NO_EMPTY_VALUE'), + jest.spyOn(DefaultEnumKeyConstraints, 'NO_RESERVED_KEYWORDS') + ]; + const jestCallback = jest.fn().mockReturnValue(''); + const constrainFunction = defaultEnumKeyConstraints({NAMING_FORMATTER: jestCallback}); + const constrainedValue = constrainFunction({enumModel, constrainedEnumModel, enumKey: ''}); + expect(constrainedValue).toEqual(''); + for (const jestMockCallback of spies) { + expect(jestMockCallback).toHaveBeenCalled(); + } + }); + }); +}); From 8eab6358fd1e2c3a413474c67e96686d27f76632 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 21:05:37 +0100 Subject: [PATCH 07/45] wip: test modelnameconstrainer (aka copy from java impl) --- .../constrainer/ModelNameConstrainer.spec.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 test/generators/kotlin/constrainer/ModelNameConstrainer.spec.ts diff --git a/test/generators/kotlin/constrainer/ModelNameConstrainer.spec.ts b/test/generators/kotlin/constrainer/ModelNameConstrainer.spec.ts new file mode 100644 index 0000000000..188d25c6c0 --- /dev/null +++ b/test/generators/kotlin/constrainer/ModelNameConstrainer.spec.ts @@ -0,0 +1,57 @@ +import { KotlinDefaultConstraints } from '../../../../src/generators/kotlin/KotlinConstrainer'; +import { DefaultModelNameConstraints, defaultModelNameConstraints, ModelNameConstraints } from '../../../../src/generators/kotlin/constrainer/ModelNameConstrainer'; +describe('ModelNameConstrainer', () => { + test('should never render special chars', () => { + const constrainedKey = KotlinDefaultConstraints.modelName({modelName: '%'}); + expect(constrainedKey).toEqual('Percent'); + }); + test('should never render number as start char', () => { + const constrainedKey = KotlinDefaultConstraints.modelName({modelName: '1'}); + expect(constrainedKey).toEqual('Number_1'); + }); + test('should never contain empty name', () => { + const constrainedKey = KotlinDefaultConstraints.modelName({modelName: ''}); + expect(constrainedKey).toEqual('Empty'); + }); + test('should use constant naming format', () => { + const constrainedKey = KotlinDefaultConstraints.modelName({modelName: 'some weird_value!"#2'}); + expect(constrainedKey).toEqual('SomeWeirdValueExclamationQuotationHash_2'); + }); + test('should never render reserved keywords', () => { + const constrainedKey = KotlinDefaultConstraints.modelName({modelName: 'return'}); + expect(constrainedKey).toEqual('ReservedReturn'); + }); + describe('custom constraints', () => { + test('should be able to overwrite all hooks', () => { + const mockedConstraintCallbacks: ModelNameConstraints = { + NAMING_FORMATTER: jest.fn().mockReturnValue(''), + NO_SPECIAL_CHAR: jest.fn().mockReturnValue(''), + NO_NUMBER_START_CHAR: jest.fn().mockReturnValue(''), + NO_EMPTY_VALUE: jest.fn().mockReturnValue(''), + NO_RESERVED_KEYWORDS: jest.fn().mockReturnValue('') + }; + const constrainFunction = defaultModelNameConstraints(mockedConstraintCallbacks); + constrainFunction({modelName: ''}); + //Expect all callbacks to be called + for (const jestMockCallback of Object.values(mockedConstraintCallbacks)) { + expect(jestMockCallback).toHaveBeenCalled(); + } + }); + test('should be able to overwrite one hooks', () => { + //All but NAMING_FORMATTER, as we customize that + const spies = [ + jest.spyOn(DefaultModelNameConstraints, 'NO_SPECIAL_CHAR'), + jest.spyOn(DefaultModelNameConstraints, 'NO_NUMBER_START_CHAR'), + jest.spyOn(DefaultModelNameConstraints, 'NO_EMPTY_VALUE'), + jest.spyOn(DefaultModelNameConstraints, 'NO_RESERVED_KEYWORDS') + ]; + const jestCallback = jest.fn().mockReturnValue(''); + const constrainFunction = defaultModelNameConstraints({NAMING_FORMATTER: jestCallback}); + const constrainedValue = constrainFunction({modelName: ''}); + expect(constrainedValue).toEqual(''); + for (const jestMockCallback of spies) { + expect(jestMockCallback).toHaveBeenCalled(); + } + }); + }); +}); From 8e5814d98411ed3b989db27a3110ff1489caf29e Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 21:07:07 +0100 Subject: [PATCH 08/45] wip: test propertykeyconstrainer (aka copy from java impl) --- .../PropertyKeyConstrainer.spec.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/generators/kotlin/constrainer/PropertyKeyConstrainer.spec.ts diff --git a/test/generators/kotlin/constrainer/PropertyKeyConstrainer.spec.ts b/test/generators/kotlin/constrainer/PropertyKeyConstrainer.spec.ts new file mode 100644 index 0000000000..a57c4c58cd --- /dev/null +++ b/test/generators/kotlin/constrainer/PropertyKeyConstrainer.spec.ts @@ -0,0 +1,90 @@ +import { KotlinDefaultConstraints } from '../../../../src/generators/kotlin/KotlinConstrainer'; +import { ConstrainedObjectModel, ConstrainedObjectPropertyModel, ObjectModel, ObjectPropertyModel } from '../../../../src'; +import { DefaultPropertyKeyConstraints, defaultPropertyKeyConstraints, PropertyKeyConstraintOptions } from '../../../../src/generators/kotlin/constrainer/PropertyKeyConstrainer'; +describe('PropertyKeyConstrainer', () => { + const objectModel = new ObjectModel('test', undefined, {}); + const constrainedObjectModel = new ConstrainedObjectModel('test', undefined, '', {}); + + const constrainPropertyName = (propertyName: string) => { + const objectPropertyModel = new ObjectPropertyModel(propertyName, false, objectModel); + const constrainedObjectPropertyModel = new ConstrainedObjectPropertyModel('', '', objectPropertyModel.required, constrainedObjectModel); + return KotlinDefaultConstraints.propertyKey({constrainedObjectModel, objectModel, objectPropertyModel, constrainedObjectPropertyModel }); + }; + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should never render special chars', () => { + const constrainedKey = constrainPropertyName('%'); + expect(constrainedKey).toEqual('percent'); + }); + test('should not render number as start char', () => { + const constrainedKey = constrainPropertyName('1'); + expect(constrainedKey).toEqual('number_1'); + }); + test('should never contain empty name', () => { + const constrainedKey = constrainPropertyName(''); + expect(constrainedKey).toEqual('empty'); + }); + test('should use constant naming format', () => { + const constrainedKey = constrainPropertyName('some weird_value!"#2'); + expect(constrainedKey).toEqual('someWeirdValueExclamationQuotationHash_2'); + }); + test('should not contain duplicate properties', () => { + const objectModel = new ObjectModel('test', undefined, {}); + const constrainedObjectModel = new ConstrainedObjectModel('test', undefined, '', {}); + const objectPropertyModel = new ObjectPropertyModel('reservedReturn', false, objectModel); + const constrainedObjectPropertyModel = new ConstrainedObjectPropertyModel('reservedReturn', '', objectPropertyModel.required, constrainedObjectModel); + const objectPropertyModel2 = new ObjectPropertyModel('return', false, objectModel); + const constrainedObjectPropertyModel2 = new ConstrainedObjectPropertyModel('return', '', objectPropertyModel.required, constrainedObjectModel); + constrainedObjectModel.properties['reservedReturn'] = constrainedObjectPropertyModel; + constrainedObjectModel.properties['return'] = constrainedObjectPropertyModel2; + const constrainedKey = KotlinDefaultConstraints.propertyKey({constrainedObjectModel, objectModel, objectPropertyModel: objectPropertyModel2, constrainedObjectPropertyModel: constrainedObjectPropertyModel2}); + expect(constrainedKey).toEqual('reservedReservedReturn'); + }); + test('should never render reserved keywords', () => { + const constrainedKey = constrainPropertyName('return'); + expect(constrainedKey).toEqual('reservedReturn'); + }); + describe('custom constraints', () => { + test('should be able to overwrite all hooks', () => { + const mockedConstraintCallbacks: Partial = { + NAMING_FORMATTER: jest.fn().mockReturnValue(''), + NO_SPECIAL_CHAR: jest.fn().mockReturnValue(''), + NO_NUMBER_START_CHAR: jest.fn().mockReturnValue(''), + NO_EMPTY_VALUE: jest.fn().mockReturnValue(''), + NO_RESERVED_KEYWORDS: jest.fn().mockReturnValue('') + }; + const constrainFunction = defaultPropertyKeyConstraints(mockedConstraintCallbacks); + const objectPropertyModel = new ObjectPropertyModel('', false, objectModel); + const constrainedObjectPropertyModel = new ConstrainedObjectPropertyModel('', '', objectPropertyModel.required, constrainedObjectModel); + constrainFunction({constrainedObjectModel, objectModel, objectPropertyModel, constrainedObjectPropertyModel}); + //Expect all callbacks to be called + for (const jestMockCallback of Object.values(mockedConstraintCallbacks)) { + expect(jestMockCallback).toHaveBeenCalled(); + } + }); + test('should be able to overwrite one hooks', () => { + //All but NAMING_FORMATTER, as we customize that + const spies = [ + jest.spyOn(DefaultPropertyKeyConstraints, 'NO_SPECIAL_CHAR'), + jest.spyOn(DefaultPropertyKeyConstraints, 'NO_NUMBER_START_CHAR'), + jest.spyOn(DefaultPropertyKeyConstraints, 'NO_EMPTY_VALUE'), + jest.spyOn(DefaultPropertyKeyConstraints, 'NO_RESERVED_KEYWORDS'), + jest.spyOn(DefaultPropertyKeyConstraints, 'NO_DUPLICATE_PROPERTIES') + ]; + const overwrittenDefaultFunction = jest.spyOn(DefaultPropertyKeyConstraints, 'NAMING_FORMATTER'); + const jestCallback = jest.fn().mockReturnValue(''); + const constrainFunction = defaultPropertyKeyConstraints({NAMING_FORMATTER: jestCallback}); + const objectPropertyModel = new ObjectPropertyModel('', false, objectModel); + const constrainedObjectPropertyModel = new ConstrainedObjectPropertyModel('', '', objectPropertyModel.required, constrainedObjectModel); + const constrainedValue = constrainFunction({constrainedObjectModel, objectModel, objectPropertyModel, constrainedObjectPropertyModel}); + expect(constrainedValue).toEqual(''); + expect(jestCallback).toHaveBeenCalled(); + expect(overwrittenDefaultFunction).not.toHaveBeenCalled(); + for (const jestMockCallback of spies) { + expect(jestMockCallback).toHaveBeenCalled(); + } + }); + }); +}); From b218b5f5bd76cd52258d9b305433dddf98e3e1db Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 21:14:28 +0100 Subject: [PATCH 09/45] wip: get rid of some code smells --- src/generators/kotlin/KotlinConstrainer.ts | 2 +- src/generators/kotlin/constrainer/EnumConstrainer.ts | 6 +++--- src/generators/kotlin/presets/DescriptionPreset.ts | 6 ++---- test/generators/kotlin/KotlinConstrainer.spec.ts | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/generators/kotlin/KotlinConstrainer.ts b/src/generators/kotlin/KotlinConstrainer.ts index 267c8d13c8..a5510e43e5 100644 --- a/src/generators/kotlin/KotlinConstrainer.ts +++ b/src/generators/kotlin/KotlinConstrainer.ts @@ -1,4 +1,4 @@ -import { ConstrainedEnumValueModel, MetaModel } from 'models'; +import { ConstrainedEnumValueModel } from 'models'; import { TypeMapping } from '../../helpers'; import { defaultEnumKeyConstraints, defaultEnumValueConstraints } from './constrainer/EnumConstrainer'; import { defaultModelNameConstraints } from './constrainer/ModelNameConstrainer'; diff --git a/src/generators/kotlin/constrainer/EnumConstrainer.ts b/src/generators/kotlin/constrainer/EnumConstrainer.ts index 74b29b50d7..d67c30ff46 100644 --- a/src/generators/kotlin/constrainer/EnumConstrainer.ts +++ b/src/generators/kotlin/constrainer/EnumConstrainer.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ConstrainedEnumModel, EnumModel } from '../../../models'; -import { NO_NUMBER_START_CHAR, NO_DUPLICATE_ENUM_KEYS, NO_EMPTY_VALUE, NO_RESERVED_KEYWORDS} from '../../../helpers/Constraints'; +import { NO_NUMBER_START_CHAR, NO_DUPLICATE_ENUM_KEYS, NO_EMPTY_VALUE, NO_RESERVED_KEYWORDS} from '../../../helpers'; import { FormatHelpers, EnumKeyConstraint, EnumValueConstraint} from '../../../helpers'; -import {isInvalidKotlinEnumKey, isReservedKotlinKeyword} from '../Constants'; +import {isInvalidKotlinEnumKey} from '../Constants'; export type ModelEnumKeyConstraints = { NO_SPECIAL_CHAR: (value: string) => string; @@ -38,7 +38,7 @@ export function defaultEnumKeyConstraints(customConstraints?: Partialitem; - - const properties = Object.keys(obj.properties) - .map(key => obj.properties[key]) + const properties = Object.keys(item.properties) + .map(key => item.properties[key]) .map(model => { const property = `@property ${model.propertyName}`; const desc = model.property.originalInput['description']; diff --git a/test/generators/kotlin/KotlinConstrainer.spec.ts b/test/generators/kotlin/KotlinConstrainer.spec.ts index 1722b3b43b..e8642e1088 100644 --- a/test/generators/kotlin/KotlinConstrainer.spec.ts +++ b/test/generators/kotlin/KotlinConstrainer.spec.ts @@ -1,5 +1,5 @@ import { KotlinDefaultTypeMapping } from '../../../src/generators/kotlin/KotlinConstrainer'; -import { KotlinGenerator, KotlinOptions } from '../../../src/generators/kotlin'; +import { KotlinGenerator, KotlinOptions } from '../../../src'; import { ConstrainedAnyModel, ConstrainedArrayModel, ConstrainedBooleanModel, ConstrainedDictionaryModel, ConstrainedEnumModel, ConstrainedEnumValueModel, ConstrainedFloatModel, ConstrainedIntegerModel, ConstrainedObjectModel, ConstrainedReferenceModel, ConstrainedStringModel, ConstrainedTupleModel, ConstrainedTupleValueModel, ConstrainedUnionModel } from '../../../src'; describe('KotlinConstrainer', () => { describe('ObjectModel', () => { From 06eeb9cf9dfddd8f8a66fcf5f22ed1fa0f999b39 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 3 Jan 2023 21:16:06 +0100 Subject: [PATCH 10/45] wip: apply some lint fixes --- src/generators/kotlin/KotlinConstrainer.ts | 4 ++-- src/generators/kotlin/presets/DescriptionPreset.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/generators/kotlin/KotlinConstrainer.ts b/src/generators/kotlin/KotlinConstrainer.ts index a5510e43e5..22dfd458e1 100644 --- a/src/generators/kotlin/KotlinConstrainer.ts +++ b/src/generators/kotlin/KotlinConstrainer.ts @@ -74,7 +74,7 @@ export const KotlinDefaultTypeMapping: TypeMapping = { return 'Any'; }, Float ({ constrainedModel }): string { - const format = constrainedModel.originalInput && constrainedModel.originalInput['format'] + const format = constrainedModel.originalInput && constrainedModel.originalInput['format']; return format === 'float' ? 'Float' : 'Double'; }, Integer ({ constrainedModel }): string { @@ -82,7 +82,7 @@ export const KotlinDefaultTypeMapping: TypeMapping = { return format === 'long' || format === 'int64' ? 'Long' : 'Int'; }, String ({ constrainedModel }): string { - const format = constrainedModel.originalInput && constrainedModel.originalInput['format'] + const format = constrainedModel.originalInput && constrainedModel.originalInput['format']; switch (format) { case 'date': { return 'java.time.LocalDate'; diff --git a/src/generators/kotlin/presets/DescriptionPreset.ts b/src/generators/kotlin/presets/DescriptionPreset.ts index bb483199f1..beb4b98e92 100644 --- a/src/generators/kotlin/presets/DescriptionPreset.ts +++ b/src/generators/kotlin/presets/DescriptionPreset.ts @@ -17,7 +17,7 @@ function renderDescription({ renderer, content, item }: { if (item instanceof ConstrainedObjectModel) { const properties = Object.keys(item.properties) - .map(key => item.properties[key]) + .map(key => item.properties[`${key}`]) .map(model => { const property = `@property ${model.propertyName}`; const desc = model.property.originalInput['description']; From a1457015b613322c28ef0b604d7dc86ebda202c1 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 6 Jan 2023 16:51:45 +0100 Subject: [PATCH 11/45] wip: somewhat test generator --- .../__snapshots__/index.spec.ts.snap | 12 + src/generators/kotlin/KotlinGenerator.ts | 19 +- .../generators/kotlin/KotlinGenerator.spec.ts | 289 ++++++++++-------- .../KotlinGenerator.spec.ts.snap | 219 +++++-------- .../DescriptionPreset.spec.ts.snap | 45 ++- 5 files changed, 288 insertions(+), 296 deletions(-) create mode 100644 examples/generate-kotlin-enums/__snapshots__/index.spec.ts.snap diff --git a/examples/generate-kotlin-enums/__snapshots__/index.spec.ts.snap b/examples/generate-kotlin-enums/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..cde2c4f39d --- /dev/null +++ b/examples/generate-kotlin-enums/__snapshots__/index.spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to render Kotlin Enums and should log expected output to console 1`] = ` +Array [ + "enum class Protocol(val value: Any) { + HTTP(\\"HTTP\\"), + NUMBER_1(1), + HTTPS(\\"HTTPS\\"), + TRUE(true); +}", +] +`; diff --git a/src/generators/kotlin/KotlinGenerator.ts b/src/generators/kotlin/KotlinGenerator.ts index 5f1aada5ce..c623a34b69 100644 --- a/src/generators/kotlin/KotlinGenerator.ts +++ b/src/generators/kotlin/KotlinGenerator.ts @@ -90,21 +90,22 @@ export class KotlinGenerator extends AbstractGenerator { - if (isReservedKotlinKeyword(options.packageName)) { - throw new Error(`You cannot use reserved Kotlin keyword (${options.packageName}) as package name, please use another.`); - } - const outputModel = await this.render(model, inputModel); - const modelDependencies = model.getNearestDependencies().map((dependencyModel) => { - return `import ${options.packageName}.${dependencyModel.name};`; - }); - const outputContent = `package ${options.packageName}; -${modelDependencies.join('\n')} + + const packageName = this.sanitizePackageName(options.packageName); + const outputContent = `package ${packageName} ${outputModel.dependencies.join('\n')} ${outputModel.result}`; return RenderOutput.toRenderOutput({result: outputContent, renderedName: outputModel.renderedName, dependencies: outputModel.dependencies}); } + private sanitizePackageName(packageName: string): string { + return packageName + .split('.') + .map(subpackage => isReservedKotlinKeyword(subpackage, true) ? `\`${subpackage}\`` : subpackage) + .join('.'); + } + async renderClass(model: ConstrainedObjectModel, inputModel: InputMetaModel): Promise { const presets = this.getPresets('class'); const renderer = new ClassRenderer(this.options, this, presets, model, inputModel); diff --git a/test/generators/kotlin/KotlinGenerator.spec.ts b/test/generators/kotlin/KotlinGenerator.spec.ts index 0f17ca9422..c2599a86cd 100644 --- a/test/generators/kotlin/KotlinGenerator.spec.ts +++ b/test/generators/kotlin/KotlinGenerator.spec.ts @@ -1,132 +1,183 @@ -import { KotlinGenerator } from '../../../src/generators/kotlin'; +import { KotlinGenerator } from '../../../src'; describe('KotlinGenerator', () => { let generator: KotlinGenerator; beforeEach(() => { generator = new KotlinGenerator(); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should not render reserved keyword', async () => { + const doc = { + $id: 'Address', + type: 'object', + properties: { + class: { type: 'string' }, + }, + additionalProperties: false + }; - describe('Enum', () => { - test('should render `enum` with mixed types (union type)', async () => { - const doc = { - $id: 'Things', - enum: ['Texas', 1, '1', false, { test: 'test' }], - }; - const models = await generator.generate(doc); - expect(models).toHaveLength(1); - expect(models[0].result).toMatchSnapshot(); - }); - - test('should work custom preset for `enum` type', async () => { - const doc = { - $id: 'CustomEnum', - type: 'string', - enum: ['Texas', 'Alabama', 'California'], - }; - - generator = new KotlinGenerator({ - presets: [ - { - enum: { - self({ content }) { - return content; - }, - } - } - ] - }); - - const models = await generator.generate(doc); - expect(models).toHaveLength(1); - expect(models[0].result).toMatchSnapshot(); - }); - - test('should render enums with translated special characters', async () => { - const doc = { - $id: 'States', - enum: ['test+', '$test', 'test-', 'test?!', '*test'] - }; - const models = await generator.generate(doc); - expect(models).toHaveLength(1); - expect(models[0].result).toMatchSnapshot(); - }); + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); }); - describe('Class', () => { - test('should not render reserved keyword', async () => { - const doc = { - $id: 'Address', - type: 'object', - properties: { - enum: { type: 'string' }, - reservedEnum: { type: 'string' } - }, - additionalProperties: false - }; - - const models = await generator.generate(doc); - expect(models).toHaveLength(1); - expect(models[0].result).toMatchSnapshot(); - }); - - test('should render `class` type', async () => { - const doc = { - $id: 'Address', - type: 'object', - properties: { - street_name: { type: 'string' }, - city: { type: 'string', description: 'City description' }, - state: { type: 'string' }, - house_number: { type: 'number' }, - marriage: { type: 'boolean', description: 'Status if marriage live in given house' }, - members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }, - array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] }, - }, - patternProperties: { - '^S(.?*)test&': { - type: 'string' - } + + test('should render `data class` type', async () => { + const doc = { + $id: 'Address', + type: 'object', + properties: { + street_name: { type: 'string' }, + city: { type: 'string', description: 'City description' }, + state: { type: 'string' }, + house_number: { type: 'number' }, + marriage: { type: 'boolean', description: 'Status if marriage live in given house' }, + members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }, + array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] }, + date: { type: 'string', format: 'date' }, + time: { type: 'string', format: 'time' }, + dateTime: { type: 'string', format: 'date-time' }, + binary: { type: 'string', format: 'binary' }, + }, + patternProperties: { + '^S(.?*)test&': { + type: 'string' + } + }, + required: ['street_name', 'city', 'state', 'house_number', 'array_type'], + }; + + const expectedDependencies = []; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + expect(models[0].dependencies).toEqual(expectedDependencies); + }); + + test('should render `enum class` type (string type)', async () => { + const doc = { + $id: 'States', + type: 'string', + enum: ['Texas', 'Alabama', 'California', 'New York'], + }; + const expectedDependencies = []; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + expect(models[0].dependencies).toEqual(expectedDependencies); + }); + + test('should render `enum` type (integer type)', async () => { + const doc = { + $id: 'Numbers', + type: 'integer', + enum: [0, 1, 2, 3], + }; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + test('should render `enum` type (union type)', async () => { + const doc = { + $id: 'Union', + type: ['string', 'integer', 'boolean'], + enum: ['Texas', 'Alabama', 0, 1, '1', true, {test: 'test'}], + }; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + test('should render enums with translated special characters', async () => { + const doc = { + $id: 'States', + enum: ['test+', 'test', 'test-', 'test?!', '*test'] + }; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + test('should render List type for collections', async () => { + const doc = { + $id: 'CustomClass', + type: 'object', + additionalProperties: false, + properties: { + arrayType: { + type: 'array', + items: { type: 'integer'}, + additionalItems: false }, - required: ['street_name', 'city', 'state', 'house_number', 'array_type'], - }; - const expectedDependencies: string[] = []; - const models = await generator.generate(doc); - expect(models).toHaveLength(1); - expect(models[0].result).toMatchSnapshot(); - expect(models[0].dependencies).toEqual(expectedDependencies); - }); - - test('should work with custom preset for `class` type', async () => { - const doc = { - $id: 'CustomClass', - type: 'object', - properties: { - property: { type: 'string' }, + } + }; + + generator = new KotlinGenerator({ collectionType: 'List' }); + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + test('should render models and their dependencies', async () => { + const doc = { + $id: 'Address', + type: 'object', + properties: { + street_name: { type: 'string' }, + city: { type: 'string', description: 'City description' }, + state: { type: 'string' }, + house_number: { type: 'number' }, + marriage: { type: 'boolean', description: 'Status if marriage live in given house' }, + members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }, + array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] }, + other_model: { type: 'object', $id: 'OtherModel', properties: {street_name: { type: 'string' }} }, + }, + patternProperties: { + '^S(.?*)test&': { + type: 'string' } - }; - generator = new KotlinGenerator({ presets: [ - { - class: { - property({ content }) { - const annotation = 'test1'; - return `${annotation}\n${content}`; - }, - getter({ content }) { - const annotation = 'test2'; - return `${annotation}\n${content}`; - }, - setter({ content }) { - const annotation = 'test3'; - return `${annotation}\n${content}`; - }, - } + }, + required: ['street_name', 'city', 'state', 'house_number', 'array_type'], + }; + const config = {packageName: 'test.package'}; + const models = await generator.generateCompleteModels(doc, config); + expect(models).toHaveLength(2); + expect(models[0].result).toMatchSnapshot(); + expect(models[1].result).toMatchSnapshot(); + }); + test('should escape reserved keywords in package name', async () => { + const doc = { + $id: 'Address', + type: 'object', + properties: { + street_name: { type: 'string' }, + city: { type: 'string', description: 'City description' }, + state: { type: 'string' }, + house_number: { type: 'number' }, + marriage: { type: 'boolean', description: 'Status if marriage live in given house' }, + members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }, + array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] }, + }, + patternProperties: { + '^S(.?*)test&': { + type: 'string' } - ] }); - const expectedDependencies: string[] = []; - - const models = await generator.generate(doc); - expect(models).toHaveLength(1); - expect(models[0].result).toMatchSnapshot(); - expect(models[0].dependencies).toEqual(expectedDependencies); - }); + }, + required: ['street_name', 'city', 'state', 'house_number', 'array_type'], + }; + const config = {packageName: 'test.class.package'}; + const models = await generator.generateCompleteModels(doc, config); + + const expectedPackageDeclaration = 'package test.`class`.`package'; + expect(models[0].result).toContain(expectedPackageDeclaration); }); }); diff --git a/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap b/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap index 44149ca720..82125a978d 100644 --- a/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap +++ b/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap @@ -1,166 +1,95 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`KotlinGenerator Class should not render reserved keyword 1`] = ` -"public class Address { - private enum; - private reservedEnum; - - public getEnum() { return this.enum; } - public void setEnum( enum) { this.enum = enum; } - - public getReservedEnum() { return this.reservedEnum; } - public void setReservedEnum( reservedEnum) { this.reservedEnum = reservedEnum; } -}" +exports[`KotlinGenerator should not render reserved keyword 1`] = ` +"data class Address( + val reservedClass: String, +)" `; -exports[`KotlinGenerator Class should render \`class\` type 1`] = ` -"public class Address { - private streetName; - private city; - private state; - private houseNumber; - private marriage; - private members; - private arrayType; - private additionalProperties; - - public getStreetName() { return this.streetName; } - public void setStreetName( streetName) { this.streetName = streetName; } - - public getCity() { return this.city; } - public void setCity( city) { this.city = city; } - - public getState() { return this.state; } - public void setState( state) { this.state = state; } - - public getHouseNumber() { return this.houseNumber; } - public void setHouseNumber( houseNumber) { this.houseNumber = houseNumber; } - - public getMarriage() { return this.marriage; } - public void setMarriage( marriage) { this.marriage = marriage; } - - public getMembers() { return this.members; } - public void setMembers( members) { this.members = members; } - - public getArrayType() { return this.arrayType; } - public void setArrayType( arrayType) { this.arrayType = arrayType; } +exports[`KotlinGenerator should render \`data class\` type 1`] = ` +"data class Address( + val streetName: String, + val city: String, + val state: String, + val houseNumber: Double, + val marriage: Boolean, + val members: Any, + val arrayType: List, + val date: java.time.LocalDate, + val time: java.time.OffsetTime, + val dateTime: java.time.OffsetDateTime, + val binary: ByteArray, + val additionalProperties: Map, +)" +`; - public getAdditionalProperties() { return this.additionalProperties; } - public void setAdditionalProperties( additionalProperties) { this.additionalProperties = additionalProperties; } +exports[`KotlinGenerator should render \`enum class\` type (string type) 1`] = ` +"enum class States(val value: String) { + TEXAS(\\"Texas\\"), + ALABAMA(\\"Alabama\\"), + CALIFORNIA(\\"California\\"), + NEW_YORK(\\"New York\\"); }" `; -exports[`KotlinGenerator Class should work with custom preset for \`class\` type 1`] = ` -"public class CustomClass { - test1 - private property; - test1 - private additionalProperties; - - test2 - public getProperty() { return this.property; } - test3 - public void setProperty( property) { this.property = property; } - - test2 - public getAdditionalProperties() { return this.additionalProperties; } - test3 - public void setAdditionalProperties( additionalProperties) { this.additionalProperties = additionalProperties; } +exports[`KotlinGenerator should render \`enum\` type (integer type) 1`] = ` +"enum class Numbers(val value: Int) { + NUMBER_0(0), + NUMBER_1(1), + NUMBER_2(2), + NUMBER_3(3); }" `; -exports[`KotlinGenerator Enum should render \`enum\` with mixed types (union type) 1`] = ` -"public enum Things { - TEXAS(\\"Texas\\"), NUMBER_1(1), RESERVED_NUMBER_1(\\"1\\"), FALSE(\\"false\\"), CURLYLEFT_QUOTATION_TEST_QUOTATION_COLON_QUOTATION_TEST_QUOTATION_CURLYRIGHT(\\"{\\\\\\"test\\\\\\":\\\\\\"test\\\\\\"}\\"); - - private Object value; - - Things(Object value) { - this.value = value; - } - - @JsonValue - public Object getValue() { - return value; - } - - @Override - public String toString() { - return String.valueOf(value); - } - - @JsonCreator - public static Things fromValue(Object value) { - for (Things e : Things.values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException(\\"Unexpected value '\\" + value + \\"'\\"); - } +exports[`KotlinGenerator should render \`enum\` type (union type) 1`] = ` +"enum class Union(val value: Any) { + TEXAS(\\"Texas\\"), + ALABAMA(\\"Alabama\\"), + NUMBER_0(0), + NUMBER_1(1), + RESERVED_NUMBER_1(\\"1\\"), + TRUE(true), + CURLYLEFT_QUOTATION_TEST_QUOTATION_COLON_QUOTATION_TEST_QUOTATION_CURLYRIGHT(\\"{\\\\\\"test\\\\\\":\\\\\\"test\\\\\\"}\\"); }" `; -exports[`KotlinGenerator Enum should render enums with translated special characters 1`] = ` -"public enum States { - TEST_PLUS(\\"test+\\"), DOLLAR_TEST(\\"$test\\"), TEST_MINUS(\\"test-\\"), TEST_QUESTION_EXCLAMATION(\\"test?!\\"), ASTERISK_TEST(\\"*test\\"); - - private Object value; - - States(Object value) { - this.value = value; - } - - @JsonValue - public Object getValue() { - return value; - } - - @Override - public String toString() { - return String.valueOf(value); - } +exports[`KotlinGenerator should render List type for collections 1`] = ` +"data class CustomClass( + val arrayType: List, +)" +`; - @JsonCreator - public static States fromValue(Object value) { - for (States e : States.values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException(\\"Unexpected value '\\" + value + \\"'\\"); - } +exports[`KotlinGenerator should render enums with translated special characters 1`] = ` +"enum class States(val value: String) { + TEST_PLUS(\\"test+\\"), + TEST(\\"test\\"), + TEST_MINUS(\\"test-\\"), + TEST_QUESTION_EXCLAMATION(\\"test?!\\"), + ASTERISK_TEST(\\"*test\\"); }" `; -exports[`KotlinGenerator Enum should work custom preset for \`enum\` type 1`] = ` -"public enum CustomEnum { - TEXAS(\\"Texas\\"), ALABAMA(\\"Alabama\\"), CALIFORNIA(\\"California\\"); - - private Object value; - - CustomEnum(Object value) { - this.value = value; - } - - @JsonValue - public Object getValue() { - return value; - } +exports[`KotlinGenerator should render models and their dependencies 1`] = ` +"package test.\`package\` + +data class Address( + val streetName: String, + val city: String, + val state: String, + val houseNumber: Double, + val marriage: Boolean, + val members: Any, + val arrayType: List, + val otherModel: OtherModel, + val additionalProperties: Map, +)" +`; - @Override - public String toString() { - return String.valueOf(value); - } +exports[`KotlinGenerator should render models and their dependencies 2`] = ` +"package test.\`package\` - @JsonCreator - public static CustomEnum fromValue(Object value) { - for (CustomEnum e : CustomEnum.values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException(\\"Unexpected value '\\" + value + \\"'\\"); - } -}" +data class OtherModel( + val streetName: String, + val additionalProperties: Map, +)" `; diff --git a/test/generators/kotlin/presets/__snapshots__/DescriptionPreset.spec.ts.snap b/test/generators/kotlin/presets/__snapshots__/DescriptionPreset.spec.ts.snap index ea26265097..d8a4c26f41 100644 --- a/test/generators/kotlin/presets/__snapshots__/DescriptionPreset.spec.ts.snap +++ b/test/generators/kotlin/presets/__snapshots__/DescriptionPreset.spec.ts.snap @@ -1,31 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`KOTLIN_DESCRIPTION_PRESET should render description and examples for class 1`] = ` -"my description -public class Clazz { - private String prop; - private Map additionalProperties; - - my description - public String getProp() { return this.prop; } - public void setProp(String prop) { this.prop = prop; } - - my description - public Map getAdditionalProperties() { return this.additionalProperties; } - public void setAdditionalProperties(Map additionalProperties) { this.additionalProperties = additionalProperties; } -}" +"/** + * Description for class + * + * @property prop Description for prop + * @property additionalProperties + * + * Examples: + * {\\"prop\\":\\"value\\"} + */ +data class Clazz( + val prop: String, + val additionalProperties: Map, +)" `; exports[`KOTLIN_DESCRIPTION_PRESET should render description and examples for enum 1`] = ` -"my description -enum class ReservedEnum(val value: String) { - ON(\\"on\\" as String), - OFF(\\"off\\" as String); - - companion object { - fun fromValue(value: String) = - ReservedEnum.values().firstOrNull { it.value.contentEquals(value, ignoringCase = true ) } - ?: throw IllegalArgumentException(\\"Unexpected value '$value'\\") - } +"/** + * Description for enum + * + * Examples: + * value + */ +enum class Enum(val value: String) { + ON(\\"on\\"), + OFF(\\"off\\"); }" `; From ded0a9e3514fb9d1f12da41ea7bc2a22b1d08861 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Sun, 8 Jan 2023 21:22:13 +0100 Subject: [PATCH 12/45] wip: blackbox tests --- .../kotlin/renderers/ClassRenderer.ts | 12 ++- test/blackbox/README.md | 1 + test/blackbox/blackbox-kotlin.spec.ts | 73 +++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 test/blackbox/blackbox-kotlin.spec.ts diff --git a/src/generators/kotlin/renderers/ClassRenderer.ts b/src/generators/kotlin/renderers/ClassRenderer.ts index 6e44002a2c..f9ac5fdffe 100644 --- a/src/generators/kotlin/renderers/ClassRenderer.ts +++ b/src/generators/kotlin/renderers/ClassRenderer.ts @@ -9,7 +9,11 @@ import { ClassPresetType } from '../KotlinPreset'; * @extends KotlinRenderer */ export class ClassRenderer extends KotlinRenderer { - async defaultSelf(): Promise { + async defaultSelf(hasProperties: boolean): Promise { + return hasProperties ? await this.defaultWithProperties() : `class ${this.model.name} {}`; + } + + private async defaultWithProperties(): Promise { const content = [ await this.renderProperties(), await this.runAdditionalContentPreset(), @@ -38,8 +42,10 @@ ${this.indent(this.renderBlock(content, 2))} } export const KOTLIN_DEFAULT_CLASS_PRESET: ClassPresetType = { - self({ renderer }) { - return renderer.defaultSelf(); + self({ renderer, model }) { + const hasProperties = Object.keys(model.properties).length > 0; + + return renderer.defaultSelf(hasProperties); }, property({ property }) { return `val ${property.propertyName}: ${property.property.type},`; diff --git a/test/blackbox/README.md b/test/blackbox/README.md index 489f6f2a85..fe43ea2608 100644 --- a/test/blackbox/README.md +++ b/test/blackbox/README.md @@ -27,6 +27,7 @@ If you want to run the BlackBox tests locally, you have to install a couple of d - To run the `Go` BlackBox tests, you need to have GoLang installed - https://golang.org/doc/install - To run the `Python` BlackBox tests, you need to have python installed - https://www.python.org/downloads/ - To run the `Rust` BlackBox tests, you need to have rust installed - https://www.rust-lang.org/tools/install (if you are on mac you might also need to install xcode `xcode-select --install`) +- To run the `Kotlin` BlackBox tests, you need to have Kotlin installed - https://kotlinlang.org/docs/command-line.html By default, the BlackBox tests are not run with the regular `npm run test`, but can be run with `npm run test:blackbox`. Or run individual BlackBox tests you can run the commands `npm run test:blackbox:${language}` where language is one of `csharp`, `go`, `java`, `javascript`, `python`, `rust`, `typescript`, etc. diff --git a/test/blackbox/blackbox-kotlin.spec.ts b/test/blackbox/blackbox-kotlin.spec.ts new file mode 100644 index 0000000000..f437a1a14d --- /dev/null +++ b/test/blackbox/blackbox-kotlin.spec.ts @@ -0,0 +1,73 @@ +/** + * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. + * + * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. + * + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { + InputMetaModel, + InputProcessor, + KotlinFileGenerator, + KOTLIN_DEFAULT_PRESET, +} from '../../src'; +import { execCommand } from './utils/Utils'; +import filesToTest from './BlackBoxTestFiles'; + +async function generate(fileToGenerateFor: string): Promise { + const inputFileContent = await fs.promises.readFile(fileToGenerateFor); + const processor = new InputProcessor(); + const input = JSON.parse(String(inputFileContent)); + return processor.process(input); +} + +function deleteDirectoryIfExists(directory: string) { + if (fs.existsSync(directory)) { + fs.rmSync(directory, { recursive: true }); + } +} + +describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + jest.setTimeout(1000000); + const fileToGenerateFor = path.resolve(__dirname, file); + const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'kotlin'); + + let models: InputMetaModel; + beforeAll(async () => { + deleteDirectoryIfExists(outputDirectoryPath); + models = await generate(fileToGenerateFor); + }); + + describe(file, () => { + const javaGeneratorOptions = [ + { + generatorOption: {}, + description: 'default generator', + renderOutputPath: path.resolve(outputDirectoryPath, './class/default') + }, + { + generatorOption: { + presets: [ + KOTLIN_DEFAULT_PRESET + ] + }, + description: 'all common presets', + renderOutputPath: path.resolve(outputDirectoryPath, './class/commonpreset') + } + ]; + + describe.each(javaGeneratorOptions)('should be able to generate and compile Kotlin', ({ generatorOption, renderOutputPath }) => { + test('class and enums', async () => { + const generator = new KotlinFileGenerator(generatorOption); + + const generatedModels = await generator.generateToFiles(models, renderOutputPath, { packageName: 'main'}); + expect(generatedModels).not.toHaveLength(0); + + const compileCommand = `kotlinc ${path.resolve(renderOutputPath, '*.kt')} -d ${outputDirectoryPath}`; + await execCommand(compileCommand); + }); + }); + }); +}); From e02ef66ecf0ac5c1cacf118b57303b7c2b144e85 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Sun, 8 Jan 2023 21:37:56 +0100 Subject: [PATCH 13/45] wip: doc --- README.md | 4 ++++ docs/languages/Kotlin.md | 44 ++++++++++++++++++++++++++++++++++++++++ docs/usage.md | 4 ++++ 3 files changed, 52 insertions(+) create mode 100644 docs/languages/Kotlin.md diff --git a/README.md b/README.md index 49218d75f7..7caab9af45 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,10 @@ The following table provides a short summary of available features for supported Python Class and enum generation: custom indentation type and size, etc + + Kotlin + Class and enum generation: use of data classes where appropriate, custom indentation type and size, etc + ## Roadmap diff --git a/docs/languages/Kotlin.md b/docs/languages/Kotlin.md new file mode 100644 index 0000000000..8f36048ef1 --- /dev/null +++ b/docs/languages/Kotlin.md @@ -0,0 +1,44 @@ +# Kotlin WIP + +There are special use-cases that each language supports; this document pertains to **Kotlin models**. + +Since `data classes` are used for every model that has got properties, there is no need for additional settings or +features to generate `toString()`, `equals()`, `hashCode()`, getters or setters. + + + + + +- [Include KDoc for properties](#include-kdoc-for-properties) +- [Change the collection type for arrays](#change-the-collection-type-for-arrays) +- [Include Javax validation constraint annotations for properties](#include-javax-validation-constraint-annotations-for-properties) +- [Generate serializer and deserializer functionality](#generate-serializer-and-deserializer-functionality) + * [To and from JSON](#to-and-from-json) + * [To and from XML](#to-and-from-xml) + * [To and from binary](#to-and-from-binary) + + +## Include KDoc for properties + +## Change the collection type for arrays + +## Include Javax validation constraint annotations for properties + +In some cases, when you generate the models from JSON Schema, you may want to include `javax.validation.constraint` annotations. + +## Generate serializer and deserializer functionality + +The most widely used usecase for Modelina is to generate models that include serialization and deserialization functionality to convert the models into payload data. This payload data can of course be many different kinds, JSON, XML, raw binary, you name it. + +As you normally only need one library to do this, we developers can never get enough with creating new stuff, therefore there might be one specific library you need or want to integrate with. Therefore there is not one specific preset that offers everything. Below is a list of all the supported serialization presets. + +### To and from JSON +Partly supported, since say Jackson-annotations are usually not needed on Kotlin data classes. +If a need was to arise, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! + +### To and from XML +Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! + +### To and from binary +Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! + diff --git a/docs/usage.md b/docs/usage.md index 657ad06622..a718d5cae5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -171,3 +171,7 @@ Rust is one of the many output languages we support. Check out this [basic examp ## Generate Python models Python is one of the many output languages we support. Check out this [basic example for a live demonstration](../examples/generate-python-models) and the following [Python documentation for more advanced use-cases](./languages/Python.md). + +## Generate Kotlin models + +Kotlin is one of the many output languages we support. Check out this [basic example for a live demonstration](../examples/generate-kotlin-models) and the following [Kotlin documentation for more advanced use-cases](./languages/Kotlin.md). From 3e5ae827aa5d36a54c982c426f46c7a0210d7e71 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Sun, 8 Jan 2023 21:39:43 +0100 Subject: [PATCH 14/45] wip: more doc --- docs/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/README.md b/docs/README.md index d8f5f3ad20..e4c4080952 100644 --- a/docs/README.md +++ b/docs/README.md @@ -62,3 +62,4 @@ Each language has its own limitations, corner cases, and features; thus, each la - [Rust](./languages/Rust.md) - [Python](./languages/Python.md) - [TypeScript](./languages/TypeScript.md) +- [Kotlin](./languages/Kotlin.md) From 32d515819e942b856ee30b9d8960d2f1372eb4a4 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Thu, 12 Jan 2023 17:50:07 +0100 Subject: [PATCH 15/45] wip: add 'ensureFilesWritten' back(?) to KotlinFileGenerator --- src/generators/kotlin/KotlinFileGenerator.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/generators/kotlin/KotlinFileGenerator.ts b/src/generators/kotlin/KotlinFileGenerator.ts index 9814769b13..08bfe7393f 100644 --- a/src/generators/kotlin/KotlinFileGenerator.ts +++ b/src/generators/kotlin/KotlinFileGenerator.ts @@ -6,19 +6,20 @@ import { FileHelpers } from '../../helpers'; export class KotlinFileGenerator extends KotlinGenerator implements AbstractFileGenerator { /** - * Generates all the models to an output directory each model with their own separate files. - * + * Generates all the models to an output directory each model with their own separate files. + * * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: KotlinRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: KotlinRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); for (const outputModel of generatedModels) { const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.kt`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } From 7fd814d185a168f1aee78cfa2db5a7f3024e3913 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Thu, 12 Jan 2023 17:51:59 +0100 Subject: [PATCH 16/45] wip: add FileGenerators test case --- test/generators/FileGenerators.spec.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/generators/FileGenerators.spec.ts b/test/generators/FileGenerators.spec.ts index 3571d0ca35..5b338e0b9f 100644 --- a/test/generators/FileGenerators.spec.ts +++ b/test/generators/FileGenerators.spec.ts @@ -1,4 +1,18 @@ -import { FileHelpers, DartFileGenerator, OutputModel, ConstrainedAnyModel, InputMetaModel, GoFileGenerator, JavaFileGenerator, JavaScriptFileGenerator, TypeScriptFileGenerator, CSharpFileGenerator, RustFileGenerator, PythonFileGenerator } from '../../src'; +import { + FileHelpers, + DartFileGenerator, + OutputModel, + ConstrainedAnyModel, + InputMetaModel, + GoFileGenerator, + JavaFileGenerator, + JavaScriptFileGenerator, + TypeScriptFileGenerator, + CSharpFileGenerator, + RustFileGenerator, + PythonFileGenerator, + KotlinFileGenerator +} from '../../src'; import * as path from 'path'; const generatorsToTest = [ @@ -41,7 +55,12 @@ const generatorsToTest = [ generator: new PythonFileGenerator(), generatorOptions: { }, fileExtension: 'py' - } + }, + { + generator: new KotlinFileGenerator(), + generatorOptions: { packageName: 'SomePackage' }, + fileExtension: 'kt' + }, ]; describe.each(generatorsToTest)('generateToFiles', ({ generator, generatorOptions, fileExtension }) => { From c6401297fe304e9f4d693aa93cb05b8d49667ab2 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Thu, 12 Jan 2023 17:55:09 +0100 Subject: [PATCH 17/45] wip: remove duplicate import in EnumConstrainer --- src/generators/kotlin/constrainer/EnumConstrainer.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/generators/kotlin/constrainer/EnumConstrainer.ts b/src/generators/kotlin/constrainer/EnumConstrainer.ts index d67c30ff46..a047db415e 100644 --- a/src/generators/kotlin/constrainer/EnumConstrainer.ts +++ b/src/generators/kotlin/constrainer/EnumConstrainer.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ConstrainedEnumModel, EnumModel } from '../../../models'; -import { NO_NUMBER_START_CHAR, NO_DUPLICATE_ENUM_KEYS, NO_EMPTY_VALUE, NO_RESERVED_KEYWORDS} from '../../../helpers'; -import { FormatHelpers, EnumKeyConstraint, EnumValueConstraint} from '../../../helpers'; -import {isInvalidKotlinEnumKey} from '../Constants'; +import { NO_NUMBER_START_CHAR, NO_DUPLICATE_ENUM_KEYS, NO_EMPTY_VALUE, NO_RESERVED_KEYWORDS, FormatHelpers, EnumKeyConstraint, EnumValueConstraint} from '../../../helpers'; +import { isInvalidKotlinEnumKey } from '../Constants'; export type ModelEnumKeyConstraints = { NO_SPECIAL_CHAR: (value: string) => string; From 35b585b755e240f88d77ec15a8dd2c9f627afcfa Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Thu, 12 Jan 2023 18:19:50 +0100 Subject: [PATCH 18/45] wip: fix failing example tests --- .../__snapshots__/index.spec.ts.snap | 11 +++++++++ examples/generate-kotlin-models/index.spec.ts | 2 +- .../__snapshots__/index.spec.ts.snap | 23 +++++++++++++++++++ examples/kotlin-generate-kdoc/index.spec.ts | 2 +- 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 examples/generate-kotlin-models/__snapshots__/index.spec.ts.snap create mode 100644 examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap diff --git a/examples/generate-kotlin-models/__snapshots__/index.spec.ts.snap b/examples/generate-kotlin-models/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..d7ee6408d7 --- /dev/null +++ b/examples/generate-kotlin-models/__snapshots__/index.spec.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to render Kotlin Models and should log expected output to console 1`] = ` +Array [ + "data class Root( + val email: String, + val cache: Int, + val website: Website, +)", +] +`; diff --git a/examples/generate-kotlin-models/index.spec.ts b/examples/generate-kotlin-models/index.spec.ts index 8bbd5c352b..83357a37f4 100644 --- a/examples/generate-kotlin-models/index.spec.ts +++ b/examples/generate-kotlin-models/index.spec.ts @@ -7,7 +7,7 @@ describe('Should be able to render Kotlin Models', () => { }); test('and should log expected output to console', async () => { await generate(); - expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls.length).toEqual(3); expect(spy.mock.calls[0]).toMatchSnapshot(); }); }); diff --git a/examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap b/examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..e808a45f3f --- /dev/null +++ b/examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to generate JavaDocs and should log expected output to console 1`] = ` +Array [ + "/** + * Description for class + * + * @property prop Description for prop + * @property enum Description for enum + * @property nodesc + * @property additionalProperties + * + * Examples: + * {\\"prop\\":\\"value\\"}, {\\"prop\\":\\"test\\"} + */ +data class KDoc( + val prop: String, + val enum: Enum, + val nodesc: String, + val additionalProperties: Map, +)", +] +`; diff --git a/examples/kotlin-generate-kdoc/index.spec.ts b/examples/kotlin-generate-kdoc/index.spec.ts index 5792a0a381..00a88bf611 100644 --- a/examples/kotlin-generate-kdoc/index.spec.ts +++ b/examples/kotlin-generate-kdoc/index.spec.ts @@ -7,7 +7,7 @@ describe('Should be able to generate JavaDocs', () => { }); test('and should log expected output to console', async () => { await generate(); - expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls.length).toEqual(2); expect(spy.mock.calls[0]).toMatchSnapshot(); }); }); From 2e9bd79ce89b8ad84412d9bb3b812ed47234604a Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Thu, 12 Jan 2023 19:18:11 +0100 Subject: [PATCH 19/45] wip: javax validation --- .../README.md | 17 +++++ .../__snapshots__/index.spec.ts.snap | 23 +++++++ .../index.spec.ts | 13 ++++ .../index.ts | 28 ++++++++ .../package-lock.json | 10 +++ .../package.json | 10 +++ package.json | 3 +- src/generators/kotlin/KotlinGenerator.ts | 1 + src/generators/kotlin/KotlinRenderer.ts | 28 +++++++- .../kotlin/presets/ConstraintsPreset.ts | 64 +++++++++++++++++++ .../KotlinGenerator.spec.ts.snap | 2 + 11 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 examples/kotlin-generate-javax-constraint-annotation/README.md create mode 100644 examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap create mode 100644 examples/kotlin-generate-javax-constraint-annotation/index.spec.ts create mode 100644 examples/kotlin-generate-javax-constraint-annotation/index.ts create mode 100644 examples/kotlin-generate-javax-constraint-annotation/package-lock.json create mode 100644 examples/kotlin-generate-javax-constraint-annotation/package.json create mode 100644 src/generators/kotlin/presets/ConstraintsPreset.ts diff --git a/examples/kotlin-generate-javax-constraint-annotation/README.md b/examples/kotlin-generate-javax-constraint-annotation/README.md new file mode 100644 index 0000000000..ce46cf5283 --- /dev/null +++ b/examples/kotlin-generate-javax-constraint-annotation/README.md @@ -0,0 +1,17 @@ +# Javax validation constraints annotations + +A basic example that shows how Kotlin data models having `javax.validation.constraints` annotations can be generated. + +## How to run this example + +Run this example using: + +```sh +npm i && npm run start +``` + +If you are on Windows, use the `start:windows` script instead: + +```sh +npm i && npm run start:windows +``` diff --git a/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap b/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..06d40707c6 --- /dev/null +++ b/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to generate models with javax.validation.constraints annotations and should log expected output to console 1`] = ` +Array [ + "package main +import javax.validation.constraints.* + +data class JavaxAnnotation( + @get:NotNull + @get:Min(0) + val minNumberProp: Double, + @get:NotNull + @get:Max(99) + val maxNumberProp: Double, + @get:Size(min=2, max=3) + val arrayProp: List, + @get:Pattern(regexp=\\"^I_\\") + @get:Size(min=3) + val stringProp: String, + val additionalProperties: Map, +)", +] +`; diff --git a/examples/kotlin-generate-javax-constraint-annotation/index.spec.ts b/examples/kotlin-generate-javax-constraint-annotation/index.spec.ts new file mode 100644 index 0000000000..00b271863c --- /dev/null +++ b/examples/kotlin-generate-javax-constraint-annotation/index.spec.ts @@ -0,0 +1,13 @@ +const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; }); +import {generate} from './index'; + +describe('Should be able to generate models with javax.validation.constraints annotations', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + test('and should log expected output to console', async () => { + await generate(); + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + }); +}); diff --git a/examples/kotlin-generate-javax-constraint-annotation/index.ts b/examples/kotlin-generate-javax-constraint-annotation/index.ts new file mode 100644 index 0000000000..ed70e682a0 --- /dev/null +++ b/examples/kotlin-generate-javax-constraint-annotation/index.ts @@ -0,0 +1,28 @@ +import {KotlinGenerator} from '../../src'; +import {KOTLIN_CONSTRAINTS_PRESET} from '../../src/generators/kotlin/presets/ConstraintsPreset'; + +const generator = new KotlinGenerator({ + presets: [KOTLIN_CONSTRAINTS_PRESET] +}); +const jsonSchemaDraft7 = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'JavaxAnnotation', + type: 'object', + properties: { + min_number_prop: { type: 'number', minimum: 0 }, + max_number_prop: { type: 'number', exclusiveMaximum: 100 }, + array_prop: { type: 'array', minItems: 2, maxItems: 3, }, + string_prop: { type: 'string', pattern: '^I_', minLength: 3 } + }, + required: ['min_number_prop', 'max_number_prop'] +}; + +export async function generate() : Promise { + const models = await generator.generateCompleteModels(jsonSchemaDraft7, { packageName: 'main' }); + for (const model of models) { + console.log(model.result); + } +} +if (require.main === module) { + generate(); +} diff --git a/examples/kotlin-generate-javax-constraint-annotation/package-lock.json b/examples/kotlin-generate-javax-constraint-annotation/package-lock.json new file mode 100644 index 0000000000..3530ecf42b --- /dev/null +++ b/examples/kotlin-generate-javax-constraint-annotation/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "kotlin-generate-javax-constraint-annotation", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/kotlin-generate-javax-constraint-annotation/package.json b/examples/kotlin-generate-javax-constraint-annotation/package.json new file mode 100644 index 0000000000..8fa9568c1b --- /dev/null +++ b/examples/kotlin-generate-javax-constraint-annotation/package.json @@ -0,0 +1,10 @@ +{ + "config" : { "example_name" : "kotlin-generate-javax-constraint-annotation" }, + "scripts": { + "install": "cd ../.. && npm i", + "start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts", + "start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts", + "test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts", + "test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts" + } +} diff --git a/package.json b/package.json index 8fd8441eb0..50e590c136 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "test:examples:update": "npm run test:examples:regular -- -u && npm run test:examples:websites -- -u", "test:examples:regular": "cross-env CI=true jest ./examples --testPathIgnorePatterns ./examples/integrate-with-react", "test:examples:websites": "cd ./examples/integrate-with-react && npm i && npm run test", - "test:blackbox": "concurrently --group -n csharp,go,java,javascript,python,rust,typescript \"npm run test:blackbox:csharp\" \"npm run test:blackbox:go\" \"npm run test:blackbox:java\" \"npm run test:blackbox:javascript\" \"npm run test:blackbox:python\" \"npm run test:blackbox:rust\" \"npm run test:blackbox:typescript\"", + "test:blackbox": "concurrently --group -n csharp,go,java,javascript,python,rust,typescript,kotlin \"npm run test:blackbox:csharp\" \"npm run test:blackbox:go\" \"npm run test:blackbox:java\" \"npm run test:blackbox:javascript\" \"npm run test:blackbox:python\" \"npm run test:blackbox:rust\" \"npm run test:blackbox:typescript\" \"npm run test:blackbox:kotlin\"", "test:blackbox:csharp": "cross-env CI=true jest ./test/blackbox/blackbox-csharp.spec.ts", "test:blackbox:go": "cross-env CI=true jest ./test/blackbox/blackbox-go.spec.ts", "test:blackbox:java": "cross-env CI=true jest ./test/blackbox/blackbox-java.spec.ts", @@ -95,6 +95,7 @@ "test:blackbox:python": "cross-env CI=true jest ./test/blackbox/blackbox-python.spec.ts", "test:blackbox:rust": "cross-env CI=true jest ./test/blackbox/blackbox-rust.spec.ts", "test:blackbox:typescript": "cross-env CI=true jest ./test/blackbox/blackbox-typescript.spec.ts", + "test:blackbox:kotlin": "cross-env CI=true jest ./test/blackbox/blackbox-kotlin.spec.ts", "test:watch": "jest --watch", "docs": "npm run docs:markdown", "docs:markdown": "jsdoc2md lib/cjs/index.js -f lib/cjs/**/*.js > API.md", diff --git a/src/generators/kotlin/KotlinGenerator.ts b/src/generators/kotlin/KotlinGenerator.ts index c623a34b69..877ee72be7 100644 --- a/src/generators/kotlin/KotlinGenerator.ts +++ b/src/generators/kotlin/KotlinGenerator.ts @@ -95,6 +95,7 @@ export class KotlinGenerator extends AbstractGenerator extends AbstractRenderer { @@ -13,7 +13,7 @@ export abstract class KotlinRenderer, - model: RendererModelType, + model: RendererModelType, inputModel: InputMetaModel, ) { super(options, generator, presets, model, inputModel); @@ -26,4 +26,28 @@ export abstract class KotlinRenderer, prefix: 'field:' | 'get:' = 'get:'): string { + const name = `@${prefix}${FormatHelpers.upperFirst(annotationName)}`; + + if (value === undefined) { + return name; + } + + if (typeof value !== 'object') { + return `${name}(${value})`; + } + + const values = concatenateEntries(Object.entries(value || {})); + return `${name}(${values})`; + } +} + +function concatenateEntries(entries: [string, unknown][] = []): string { + return entries.map(([paramName, newValue]) => { + if (paramName && newValue !== undefined) { + return `${paramName}=${newValue}`; + } + return newValue; + }).filter(v => v !== undefined).join(', '); } diff --git a/src/generators/kotlin/presets/ConstraintsPreset.ts b/src/generators/kotlin/presets/ConstraintsPreset.ts new file mode 100644 index 0000000000..0d03460a61 --- /dev/null +++ b/src/generators/kotlin/presets/ConstraintsPreset.ts @@ -0,0 +1,64 @@ +import { ConstrainedArrayModel, ConstrainedFloatModel, ConstrainedIntegerModel, ConstrainedStringModel } from '../../../models'; +import { KotlinPreset } from '../KotlinPreset'; + +export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { + class: { + self({renderer, content}) { + renderer.addDependency('import javax.validation.constraints.*'); + return content; + }, + property({ renderer, property, content}) { + const annotations: string[] = []; + + if (property.required) { + annotations.push(renderer.renderAnnotation('NotNull')); + } + const originalInput = property.property.originalInput; + + // string + if (property.property instanceof ConstrainedStringModel) { + const pattern = originalInput['pattern']; + if (pattern !== undefined) { + annotations.push(renderer.renderAnnotation('Pattern', { regexp: `"${pattern}"` })); + } + const minLength = originalInput['minLength']; + const maxLength = originalInput['maxLength']; + if (minLength !== undefined || maxLength !== undefined) { + annotations.push(renderer.renderAnnotation('Size', { min: minLength, max: maxLength })); + } + } + + // number/integer + if (property.property instanceof ConstrainedFloatModel || + property.property instanceof ConstrainedIntegerModel) { + const minimum = originalInput['minimum']; + if (minimum !== undefined) { + annotations.push(renderer.renderAnnotation('Min', minimum)); + } + const exclusiveMinimum = originalInput['exclusiveMinimum']; + if (exclusiveMinimum !== undefined) { + annotations.push(renderer.renderAnnotation('Min', exclusiveMinimum + 1)); + } + const maximum = originalInput['maximum']; + if (maximum !== undefined) { + annotations.push(renderer.renderAnnotation('Max', maximum)); + } + const exclusiveMaximum = originalInput['exclusiveMaximum']; + if (exclusiveMaximum !== undefined) { + annotations.push(renderer.renderAnnotation('Max', exclusiveMaximum - 1)); + } + } + + // array + if (property.property instanceof ConstrainedArrayModel) { + const minItems = originalInput['minItems']; + const maxItems = originalInput['maxItems']; + if (minItems !== undefined || maxItems !== undefined) { + annotations.push(renderer.renderAnnotation('Size', { min: minItems, max: maxItems })); + } + } + + return renderer.renderBlock([...annotations, content]); + } + } +} diff --git a/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap b/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap index 82125a978d..fe4c92ca7a 100644 --- a/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap +++ b/test/generators/kotlin/__snapshots__/KotlinGenerator.spec.ts.snap @@ -72,6 +72,7 @@ exports[`KotlinGenerator should render enums with translated special characters exports[`KotlinGenerator should render models and their dependencies 1`] = ` "package test.\`package\` + data class Address( val streetName: String, val city: String, @@ -88,6 +89,7 @@ data class Address( exports[`KotlinGenerator should render models and their dependencies 2`] = ` "package test.\`package\` + data class OtherModel( val streetName: String, val additionalProperties: Map, From 93af53d103f58a7004d62173e369b3f69a105ffe Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Thu, 12 Jan 2023 19:35:54 +0100 Subject: [PATCH 20/45] wip: tested constrain validation stuff --- .../__snapshots__/index.spec.ts.snap | 5 +---- .../index.ts | 2 +- src/generators/kotlin/KotlinRenderer.ts | 14 ++++++++++---- .../kotlin/presets/ConstraintsPreset.ts | 17 +++++++++-------- test/generators/kotlin/KotlinRenderer.spec.ts | 18 ++++++++++++++++++ 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap b/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap index 06d40707c6..bc955b5483 100644 --- a/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap +++ b/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap @@ -2,10 +2,7 @@ exports[`Should be able to generate models with javax.validation.constraints annotations and should log expected output to console 1`] = ` Array [ - "package main -import javax.validation.constraints.* - -data class JavaxAnnotation( + "data class JavaxAnnotation( @get:NotNull @get:Min(0) val minNumberProp: Double, diff --git a/examples/kotlin-generate-javax-constraint-annotation/index.ts b/examples/kotlin-generate-javax-constraint-annotation/index.ts index ed70e682a0..e485db3da6 100644 --- a/examples/kotlin-generate-javax-constraint-annotation/index.ts +++ b/examples/kotlin-generate-javax-constraint-annotation/index.ts @@ -18,7 +18,7 @@ const jsonSchemaDraft7 = { }; export async function generate() : Promise { - const models = await generator.generateCompleteModels(jsonSchemaDraft7, { packageName: 'main' }); + const models = await generator.generate(jsonSchemaDraft7); for (const model of models) { console.log(model.result); } diff --git a/src/generators/kotlin/KotlinRenderer.ts b/src/generators/kotlin/KotlinRenderer.ts index 104cb40db9..a6d9ab9995 100644 --- a/src/generators/kotlin/KotlinRenderer.ts +++ b/src/generators/kotlin/KotlinRenderer.ts @@ -27,10 +27,10 @@ ${newLiteral} */`; } - renderAnnotation(annotationName: string, value?: any | Record, prefix: 'field:' | 'get:' = 'get:'): string { - const name = `@${prefix}${FormatHelpers.upperFirst(annotationName)}`; + renderAnnotation(annotationName: string, value?: any | Record, prefix?: 'field:' | 'get:' | 'param:'): string { + const name = `@${!prefix ? '' : prefix}${FormatHelpers.upperFirst(annotationName)}`; - if (value === undefined) { + if (value === undefined || value === null) { return name; } @@ -38,7 +38,13 @@ ${newLiteral} return `${name}(${value})`; } - const values = concatenateEntries(Object.entries(value || {})); + const entries = Object.entries(value || {}) + + if (entries.length === 0) { + return name; + } + + const values = concatenateEntries(entries); return `${name}(${values})`; } } diff --git a/src/generators/kotlin/presets/ConstraintsPreset.ts b/src/generators/kotlin/presets/ConstraintsPreset.ts index 0d03460a61..407d6ec2e1 100644 --- a/src/generators/kotlin/presets/ConstraintsPreset.ts +++ b/src/generators/kotlin/presets/ConstraintsPreset.ts @@ -1,5 +1,6 @@ import { ConstrainedArrayModel, ConstrainedFloatModel, ConstrainedIntegerModel, ConstrainedStringModel } from '../../../models'; import { KotlinPreset } from '../KotlinPreset'; +import {prefix} from 'concurrently/dist/src/defaults'; export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { class: { @@ -11,7 +12,7 @@ export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { const annotations: string[] = []; if (property.required) { - annotations.push(renderer.renderAnnotation('NotNull')); + annotations.push(renderer.renderAnnotation('NotNull', null, 'get:')); } const originalInput = property.property.originalInput; @@ -19,12 +20,12 @@ export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { if (property.property instanceof ConstrainedStringModel) { const pattern = originalInput['pattern']; if (pattern !== undefined) { - annotations.push(renderer.renderAnnotation('Pattern', { regexp: `"${pattern}"` })); + annotations.push(renderer.renderAnnotation('Pattern', { regexp: `"${pattern}"` }, 'get:')); } const minLength = originalInput['minLength']; const maxLength = originalInput['maxLength']; if (minLength !== undefined || maxLength !== undefined) { - annotations.push(renderer.renderAnnotation('Size', { min: minLength, max: maxLength })); + annotations.push(renderer.renderAnnotation('Size', { min: minLength, max: maxLength }, 'get:')); } } @@ -33,19 +34,19 @@ export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { property.property instanceof ConstrainedIntegerModel) { const minimum = originalInput['minimum']; if (minimum !== undefined) { - annotations.push(renderer.renderAnnotation('Min', minimum)); + annotations.push(renderer.renderAnnotation('Min', minimum, 'get:')); } const exclusiveMinimum = originalInput['exclusiveMinimum']; if (exclusiveMinimum !== undefined) { - annotations.push(renderer.renderAnnotation('Min', exclusiveMinimum + 1)); + annotations.push(renderer.renderAnnotation('Min', exclusiveMinimum + 1), 'get:'); } const maximum = originalInput['maximum']; if (maximum !== undefined) { - annotations.push(renderer.renderAnnotation('Max', maximum)); + annotations.push(renderer.renderAnnotation('Max', maximum, 'get:')); } const exclusiveMaximum = originalInput['exclusiveMaximum']; if (exclusiveMaximum !== undefined) { - annotations.push(renderer.renderAnnotation('Max', exclusiveMaximum - 1)); + annotations.push(renderer.renderAnnotation('Max', exclusiveMaximum - 1, 'get:')); } } @@ -54,7 +55,7 @@ export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { const minItems = originalInput['minItems']; const maxItems = originalInput['maxItems']; if (minItems !== undefined || maxItems !== undefined) { - annotations.push(renderer.renderAnnotation('Size', { min: minItems, max: maxItems })); + annotations.push(renderer.renderAnnotation('Size', { min: minItems, max: maxItems }, 'get:')); } } diff --git a/test/generators/kotlin/KotlinRenderer.spec.ts b/test/generators/kotlin/KotlinRenderer.spec.ts index 55ef433aba..dc2e7d9a96 100644 --- a/test/generators/kotlin/KotlinRenderer.spec.ts +++ b/test/generators/kotlin/KotlinRenderer.spec.ts @@ -2,6 +2,7 @@ import { KotlinGenerator } from '../../../src/generators/kotlin'; import { KotlinRenderer } from '../../../src/generators/kotlin/KotlinRenderer'; import { ConstrainedObjectModel, InputMetaModel } from '../../../src/models'; import { MockKotlinRenderer } from '../../TestUtils/TestRenderers'; +import {prefix} from 'concurrently/dist/src/defaults'; describe('KotlinRenderer', () => { let renderer: KotlinRenderer; @@ -16,4 +17,21 @@ describe('KotlinRenderer', () => { */`); }); }); + + describe('renderAnnotation()', () => { + test('Should render', () => { + expect(renderer.renderAnnotation('someComment')).toEqual('@SomeComment'); + }); + test('Should be able to render multiple values', () => { + expect(renderer.renderAnnotation('someComment', {test: 1, cool: '"story"'})).toEqual('@SomeComment(test=1, cool="story")'); + }); + test('Should be able to render one value', () => { + expect(renderer.renderAnnotation('someComment', {test: '"test2"'})).toEqual('@SomeComment(test="test2")'); + }); + test('Should be able to use different prefixes', () => { + expect(renderer.renderAnnotation('someComment', null, 'get:')).toEqual('@get:SomeComment'); + expect(renderer.renderAnnotation('someComment', null, 'field:')).toEqual('@field:SomeComment'); + expect(renderer.renderAnnotation('someComment', null, 'param:')).toEqual('@param:SomeComment'); + }); + }); }); From aef13583ea3b04df0c43407ed15f2108761a0ffd Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Thu, 12 Jan 2023 19:47:07 +0100 Subject: [PATCH 21/45] wip: test preset --- .../index.ts | 6 ++-- .../java/presets/ConstraintsPreset.ts | 14 +++++----- .../kotlin/presets/ConstraintsPreset.ts | 1 - .../java/presets/ConstraintsPreset.spec.ts | 6 ++-- .../kotlin/presets/ConstraintsPreset.spec.ts | 28 +++++++++++++++++++ .../ConstraintsPreset.spec.ts.snap | 18 ++++++++++++ 6 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 test/generators/kotlin/presets/ConstraintsPreset.spec.ts create mode 100644 test/generators/kotlin/presets/__snapshots__/ConstraintsPreset.spec.ts.snap diff --git a/examples/java-generate-javax-constraint-annotation/index.ts b/examples/java-generate-javax-constraint-annotation/index.ts index 0955e3bce6..7dee9c0e96 100644 --- a/examples/java-generate-javax-constraint-annotation/index.ts +++ b/examples/java-generate-javax-constraint-annotation/index.ts @@ -1,7 +1,7 @@ -import { JavaGenerator, JAVA_CONSTRAINTS_PRESET } from '../../src'; +import { JavaGenerator, KOTLIN_CONSTRAINTS_PRESET } from '../../src'; -const generator = new JavaGenerator({ - presets: [JAVA_CONSTRAINTS_PRESET] +const generator = new JavaGenerator({ + presets: [KOTLIN_CONSTRAINTS_PRESET] }); const jsonSchemaDraft7 = { $schema: 'http://json-schema.org/draft-07/schema#', diff --git a/src/generators/java/presets/ConstraintsPreset.ts b/src/generators/java/presets/ConstraintsPreset.ts index 5ed4349d61..ac12f12bc0 100644 --- a/src/generators/java/presets/ConstraintsPreset.ts +++ b/src/generators/java/presets/ConstraintsPreset.ts @@ -1,12 +1,12 @@ import { ConstrainedArrayModel, ConstrainedFloatModel, ConstrainedIntegerModel, ConstrainedStringModel } from '../../../models'; -import { JavaPreset } from '../JavaPreset'; +import {KotlinPreset} from '../../kotlin'; /** * Preset which extends class's getters with annotations from `javax.validation.constraints` package - * + * * @implements {JavaPreset} */ -export const JAVA_CONSTRAINTS_PRESET: JavaPreset = { +export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { class: { self({renderer, content}) { renderer.addDependency('import javax.validation.constraints.*;'); @@ -15,7 +15,7 @@ export const JAVA_CONSTRAINTS_PRESET: JavaPreset = { // eslint-disable-next-line sonarjs/cognitive-complexity property({ renderer, property, content }) { const annotations: string[] = []; - + if (property.required) { annotations.push(renderer.renderAnnotation('NotNull')); } @@ -33,7 +33,7 @@ export const JAVA_CONSTRAINTS_PRESET: JavaPreset = { annotations.push(renderer.renderAnnotation('Size', { min: minLength, max: maxLength })); } } - + // number/integer if (property.property instanceof ConstrainedFloatModel || property.property instanceof ConstrainedIntegerModel) { @@ -54,7 +54,7 @@ export const JAVA_CONSTRAINTS_PRESET: JavaPreset = { annotations.push(renderer.renderAnnotation('Max', exclusiveMaximum - 1)); } } - + // array if (property.property instanceof ConstrainedArrayModel) { const minItems = originalInput['minItems']; @@ -63,7 +63,7 @@ export const JAVA_CONSTRAINTS_PRESET: JavaPreset = { annotations.push(renderer.renderAnnotation('Size', { min: minItems, max: maxItems })); } } - + return renderer.renderBlock([...annotations, content]); }, } diff --git a/src/generators/kotlin/presets/ConstraintsPreset.ts b/src/generators/kotlin/presets/ConstraintsPreset.ts index 407d6ec2e1..8935a0445d 100644 --- a/src/generators/kotlin/presets/ConstraintsPreset.ts +++ b/src/generators/kotlin/presets/ConstraintsPreset.ts @@ -1,6 +1,5 @@ import { ConstrainedArrayModel, ConstrainedFloatModel, ConstrainedIntegerModel, ConstrainedStringModel } from '../../../models'; import { KotlinPreset } from '../KotlinPreset'; -import {prefix} from 'concurrently/dist/src/defaults'; export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { class: { diff --git a/test/generators/java/presets/ConstraintsPreset.spec.ts b/test/generators/java/presets/ConstraintsPreset.spec.ts index 23e7c35593..2068811267 100644 --- a/test/generators/java/presets/ConstraintsPreset.spec.ts +++ b/test/generators/java/presets/ConstraintsPreset.spec.ts @@ -1,9 +1,9 @@ -import { JavaGenerator, JAVA_CONSTRAINTS_PRESET } from '../../../../src/generators'; +import { JavaGenerator, KOTLIN_CONSTRAINTS_PRESET } from '../../../../src/generators'; describe('JAVA_CONSTRAINTS_PRESET', () => { let generator: JavaGenerator; beforeEach(() => { - generator = new JavaGenerator({ presets: [JAVA_CONSTRAINTS_PRESET] }); + generator = new JavaGenerator({ presets: [KOTLIN_CONSTRAINTS_PRESET] }); }); test('should render constraints annotations', async () => { @@ -22,7 +22,7 @@ describe('JAVA_CONSTRAINTS_PRESET', () => { const models = await generator.generate(doc); expect(models).toHaveLength(1); - expect(models[0].result).toMatchSnapshot(); + expect(models[0].result).toMatchSnapshot(); expect(models[0].dependencies).toEqual(expectedDependencies); }); }); diff --git a/test/generators/kotlin/presets/ConstraintsPreset.spec.ts b/test/generators/kotlin/presets/ConstraintsPreset.spec.ts new file mode 100644 index 0000000000..1e742f1a47 --- /dev/null +++ b/test/generators/kotlin/presets/ConstraintsPreset.spec.ts @@ -0,0 +1,28 @@ +import { KotlinGenerator, KOTLIN_CONSTRAINTS_PRESET } from '../../../../src'; + +describe('KOTLIN_CONSTRAINTS_PRESET', () => { + let generator: KotlinGenerator; + beforeEach(() => { + generator = new KotlinGenerator({ presets: [KOTLIN_CONSTRAINTS_PRESET] }); + }); + + test('should render constraints annotations', async () => { + const doc = { + $id: 'Clazz', + type: 'object', + properties: { + min_number_prop: { type: 'number', minimum: 0 }, + max_number_prop: { type: 'number', exclusiveMaximum: 100 }, + array_prop: { type: 'array', minItems: 2, maxItems: 3, }, + string_prop: { type: 'string', pattern: '^I_', minLength: 3 } + }, + required: ['min_number_prop', 'max_number_prop'] + }; + const expectedDependencies = ['import javax.validation.constraints.*;']; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + expect(models[0].dependencies).toEqual(expectedDependencies); + }); +}); diff --git a/test/generators/kotlin/presets/__snapshots__/ConstraintsPreset.spec.ts.snap b/test/generators/kotlin/presets/__snapshots__/ConstraintsPreset.spec.ts.snap new file mode 100644 index 0000000000..431332086a --- /dev/null +++ b/test/generators/kotlin/presets/__snapshots__/ConstraintsPreset.spec.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KOTLIN_CONSTRAINTS_PRESET should render constraints annotations 1`] = ` +"data class Clazz( + @NotNull + @Min(0) + val minNumberProp: Double, + @NotNull + @Max(99) + val maxNumberProp: Double, + @Size(min=2, max=3) + val arrayProp: List, + @Pattern(regexp=\\"^I_\\") + @Size(min=3) + val stringProp: String, + val additionalProperties: Map, +)" +`; From 6df2a2025a77db9510723e9e0a856d8ee606d545 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 10:06:51 +0100 Subject: [PATCH 22/45] oops intellij renamed the Java preset aaa --- .../index.ts | 4 ++-- .../index.ts | 3 +-- src/generators/java/presets/ConstraintsPreset.ts | 4 ++-- src/generators/kotlin/presets/ConstraintsPreset.ts | 2 +- src/generators/kotlin/presets/index.ts | 1 + .../java/presets/ConstraintsPreset.spec.ts | 4 ++-- .../kotlin/presets/ConstraintsPreset.spec.ts | 3 +-- .../__snapshots__/ConstraintsPreset.spec.ts.snap | 14 +++++++------- 8 files changed, 17 insertions(+), 18 deletions(-) diff --git a/examples/java-generate-javax-constraint-annotation/index.ts b/examples/java-generate-javax-constraint-annotation/index.ts index 7dee9c0e96..b7b0572c58 100644 --- a/examples/java-generate-javax-constraint-annotation/index.ts +++ b/examples/java-generate-javax-constraint-annotation/index.ts @@ -1,7 +1,7 @@ -import { JavaGenerator, KOTLIN_CONSTRAINTS_PRESET } from '../../src'; +import { JavaGenerator, JAVA_CONSTRAINTS_PRESET } from '../../src'; const generator = new JavaGenerator({ - presets: [KOTLIN_CONSTRAINTS_PRESET] + presets: [JAVA_CONSTRAINTS_PRESET] }); const jsonSchemaDraft7 = { $schema: 'http://json-schema.org/draft-07/schema#', diff --git a/examples/kotlin-generate-javax-constraint-annotation/index.ts b/examples/kotlin-generate-javax-constraint-annotation/index.ts index e485db3da6..5456a5cd50 100644 --- a/examples/kotlin-generate-javax-constraint-annotation/index.ts +++ b/examples/kotlin-generate-javax-constraint-annotation/index.ts @@ -1,5 +1,4 @@ -import {KotlinGenerator} from '../../src'; -import {KOTLIN_CONSTRAINTS_PRESET} from '../../src/generators/kotlin/presets/ConstraintsPreset'; +import {KotlinGenerator, KOTLIN_CONSTRAINTS_PRESET} from '../../src'; const generator = new KotlinGenerator({ presets: [KOTLIN_CONSTRAINTS_PRESET] diff --git a/src/generators/java/presets/ConstraintsPreset.ts b/src/generators/java/presets/ConstraintsPreset.ts index ac12f12bc0..06fc8a965c 100644 --- a/src/generators/java/presets/ConstraintsPreset.ts +++ b/src/generators/java/presets/ConstraintsPreset.ts @@ -1,12 +1,12 @@ import { ConstrainedArrayModel, ConstrainedFloatModel, ConstrainedIntegerModel, ConstrainedStringModel } from '../../../models'; -import {KotlinPreset} from '../../kotlin'; +import {JavaPreset} from '../JavaPreset'; /** * Preset which extends class's getters with annotations from `javax.validation.constraints` package * * @implements {JavaPreset} */ -export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { +export const JAVA_CONSTRAINTS_PRESET: JavaPreset = { class: { self({renderer, content}) { renderer.addDependency('import javax.validation.constraints.*;'); diff --git a/src/generators/kotlin/presets/ConstraintsPreset.ts b/src/generators/kotlin/presets/ConstraintsPreset.ts index 8935a0445d..fa0935c956 100644 --- a/src/generators/kotlin/presets/ConstraintsPreset.ts +++ b/src/generators/kotlin/presets/ConstraintsPreset.ts @@ -61,4 +61,4 @@ export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { return renderer.renderBlock([...annotations, content]); } } -} +}; diff --git a/src/generators/kotlin/presets/index.ts b/src/generators/kotlin/presets/index.ts index e74c57b288..083899b1d8 100644 --- a/src/generators/kotlin/presets/index.ts +++ b/src/generators/kotlin/presets/index.ts @@ -1 +1,2 @@ export * from './DescriptionPreset'; +export * from './ConstraintsPreset'; diff --git a/test/generators/java/presets/ConstraintsPreset.spec.ts b/test/generators/java/presets/ConstraintsPreset.spec.ts index 2068811267..a5f4aab458 100644 --- a/test/generators/java/presets/ConstraintsPreset.spec.ts +++ b/test/generators/java/presets/ConstraintsPreset.spec.ts @@ -1,9 +1,9 @@ -import { JavaGenerator, KOTLIN_CONSTRAINTS_PRESET } from '../../../../src/generators'; +import { JavaGenerator, JAVA_CONSTRAINTS_PRESET } from '../../../../src/generators'; describe('JAVA_CONSTRAINTS_PRESET', () => { let generator: JavaGenerator; beforeEach(() => { - generator = new JavaGenerator({ presets: [KOTLIN_CONSTRAINTS_PRESET] }); + generator = new JavaGenerator({ presets: [JAVA_CONSTRAINTS_PRESET] }); }); test('should render constraints annotations', async () => { diff --git a/test/generators/kotlin/presets/ConstraintsPreset.spec.ts b/test/generators/kotlin/presets/ConstraintsPreset.spec.ts index 1e742f1a47..4a7cc976c8 100644 --- a/test/generators/kotlin/presets/ConstraintsPreset.spec.ts +++ b/test/generators/kotlin/presets/ConstraintsPreset.spec.ts @@ -1,5 +1,4 @@ import { KotlinGenerator, KOTLIN_CONSTRAINTS_PRESET } from '../../../../src'; - describe('KOTLIN_CONSTRAINTS_PRESET', () => { let generator: KotlinGenerator; beforeEach(() => { @@ -18,7 +17,7 @@ describe('KOTLIN_CONSTRAINTS_PRESET', () => { }, required: ['min_number_prop', 'max_number_prop'] }; - const expectedDependencies = ['import javax.validation.constraints.*;']; + const expectedDependencies = ['import javax.validation.constraints.*']; const models = await generator.generate(doc); expect(models).toHaveLength(1); diff --git a/test/generators/kotlin/presets/__snapshots__/ConstraintsPreset.spec.ts.snap b/test/generators/kotlin/presets/__snapshots__/ConstraintsPreset.spec.ts.snap index 431332086a..cb10445098 100644 --- a/test/generators/kotlin/presets/__snapshots__/ConstraintsPreset.spec.ts.snap +++ b/test/generators/kotlin/presets/__snapshots__/ConstraintsPreset.spec.ts.snap @@ -2,16 +2,16 @@ exports[`KOTLIN_CONSTRAINTS_PRESET should render constraints annotations 1`] = ` "data class Clazz( - @NotNull - @Min(0) + @get:NotNull + @get:Min(0) val minNumberProp: Double, - @NotNull - @Max(99) + @get:NotNull + @get:Max(99) val maxNumberProp: Double, - @Size(min=2, max=3) + @get:Size(min=2, max=3) val arrayProp: List, - @Pattern(regexp=\\"^I_\\") - @Size(min=3) + @get:Pattern(regexp=\\"^I_\\") + @get:Size(min=3) val stringProp: String, val additionalProperties: Map, )" From b431ce0a8d9ac062b63ea96e2f0130798f1d8436 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 10:38:28 +0100 Subject: [PATCH 23/45] refactor ConstraintsPreset --- .../kotlin/presets/ConstraintsPreset.ts | 121 +++++++++++------- 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/src/generators/kotlin/presets/ConstraintsPreset.ts b/src/generators/kotlin/presets/ConstraintsPreset.ts index fa0935c956..30654506d4 100644 --- a/src/generators/kotlin/presets/ConstraintsPreset.ts +++ b/src/generators/kotlin/presets/ConstraintsPreset.ts @@ -1,5 +1,12 @@ -import { ConstrainedArrayModel, ConstrainedFloatModel, ConstrainedIntegerModel, ConstrainedStringModel } from '../../../models'; +import { + ConstrainedArrayModel, + ConstrainedFloatModel, + ConstrainedIntegerModel, + ConstrainedMetaModel, + ConstrainedStringModel +} from '../../../models'; import { KotlinPreset } from '../KotlinPreset'; +import {ClassRenderer} from '../renderers/ClassRenderer'; export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { class: { @@ -8,57 +15,79 @@ export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { return content; }, property({ renderer, property, content}) { - const annotations: string[] = []; + let annotations: string[] = []; if (property.required) { - annotations.push(renderer.renderAnnotation('NotNull', null, 'get:')); + annotations = [...annotations, renderer.renderAnnotation('NotNull', null, 'get:')]; } - const originalInput = property.property.originalInput; - // string - if (property.property instanceof ConstrainedStringModel) { - const pattern = originalInput['pattern']; - if (pattern !== undefined) { - annotations.push(renderer.renderAnnotation('Pattern', { regexp: `"${pattern}"` }, 'get:')); - } - const minLength = originalInput['minLength']; - const maxLength = originalInput['maxLength']; - if (minLength !== undefined || maxLength !== undefined) { - annotations.push(renderer.renderAnnotation('Size', { min: minLength, max: maxLength }, 'get:')); - } - } - - // number/integer - if (property.property instanceof ConstrainedFloatModel || - property.property instanceof ConstrainedIntegerModel) { - const minimum = originalInput['minimum']; - if (minimum !== undefined) { - annotations.push(renderer.renderAnnotation('Min', minimum, 'get:')); - } - const exclusiveMinimum = originalInput['exclusiveMinimum']; - if (exclusiveMinimum !== undefined) { - annotations.push(renderer.renderAnnotation('Min', exclusiveMinimum + 1), 'get:'); - } - const maximum = originalInput['maximum']; - if (maximum !== undefined) { - annotations.push(renderer.renderAnnotation('Max', maximum, 'get:')); - } - const exclusiveMaximum = originalInput['exclusiveMaximum']; - if (exclusiveMaximum !== undefined) { - annotations.push(renderer.renderAnnotation('Max', exclusiveMaximum - 1, 'get:')); - } - } - - // array - if (property.property instanceof ConstrainedArrayModel) { - const minItems = originalInput['minItems']; - const maxItems = originalInput['maxItems']; - if (minItems !== undefined || maxItems !== undefined) { - annotations.push(renderer.renderAnnotation('Size', { min: minItems, max: maxItems }, 'get:')); - } - } + annotations = [...annotations, ...getTypeSpecificAnnotations(property.property, renderer)]; return renderer.renderBlock([...annotations, content]); } } }; + +function getTypeSpecificAnnotations(property: ConstrainedMetaModel, renderer: ClassRenderer): string[] { + if (property instanceof ConstrainedStringModel) { + return getStringAnnotations(property, renderer); + } else if (property instanceof ConstrainedFloatModel || property instanceof ConstrainedIntegerModel) { + return getNumericAnnotations(property, renderer); + } else if (property instanceof ConstrainedArrayModel) { + return getArrayAnnotations(property, renderer); + } + + return []; +} + +function getStringAnnotations(property: ConstrainedStringModel, renderer: ClassRenderer): string[] { + const annotations: string[] = []; + const originalInput = property.originalInput; + const pattern = originalInput['pattern']; + if (pattern !== undefined) { + annotations.push(renderer.renderAnnotation('Pattern', { regexp: `"${pattern}"` }, 'get:')); + } + const minLength = originalInput['minLength']; + const maxLength = originalInput['maxLength']; + if (minLength !== undefined || maxLength !== undefined) { + annotations.push(renderer.renderAnnotation('Size', { min: minLength, max: maxLength }, 'get:')); + } + return annotations; +} + +function getNumericAnnotations(property: ConstrainedIntegerModel | ConstrainedFloatModel, renderer: ClassRenderer): string[] { + const annotations: string[] = []; + const originalInput = property.originalInput; + + const minimum = originalInput['minimum']; + if (minimum !== undefined) { + annotations.push(renderer.renderAnnotation('Min', minimum, 'get:')); + } + const exclusiveMinimum = originalInput['exclusiveMinimum']; + if (exclusiveMinimum !== undefined) { + annotations.push(renderer.renderAnnotation('Min', exclusiveMinimum + 1), 'get:'); + } + const maximum = originalInput['maximum']; + if (maximum !== undefined) { + annotations.push(renderer.renderAnnotation('Max', maximum, 'get:')); + } + const exclusiveMaximum = originalInput['exclusiveMaximum']; + if (exclusiveMaximum !== undefined) { + annotations.push(renderer.renderAnnotation('Max', exclusiveMaximum - 1, 'get:')); + } + + return annotations; +} + +function getArrayAnnotations(property: ConstrainedArrayModel, renderer: ClassRenderer): string[] { + const annotations: string[] = []; + const originalInput = property.originalInput; + + const minItems = originalInput['minItems']; + const maxItems = originalInput['maxItems']; + if (minItems !== undefined || maxItems !== undefined) { + annotations.push(renderer.renderAnnotation('Size', { min: minItems, max: maxItems }, 'get:')); + } + + return annotations; +} From 4a442ff6c0c1c894939dc472ce6d6b62ed4ea203 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 10:54:03 +0100 Subject: [PATCH 24/45] refactor --- .../kotlin/presets/ConstraintsPreset.ts | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/src/generators/kotlin/presets/ConstraintsPreset.ts b/src/generators/kotlin/presets/ConstraintsPreset.ts index 30654506d4..6d44480bcd 100644 --- a/src/generators/kotlin/presets/ConstraintsPreset.ts +++ b/src/generators/kotlin/presets/ConstraintsPreset.ts @@ -15,13 +15,13 @@ export const KOTLIN_CONSTRAINTS_PRESET: KotlinPreset = { return content; }, property({ renderer, property, content}) { - let annotations: string[] = []; + const annotations: string[] = []; if (property.required) { - annotations = [...annotations, renderer.renderAnnotation('NotNull', null, 'get:')]; + annotations.push(renderer.renderAnnotation('NotNull', null, 'get:')); } - annotations = [...annotations, ...getTypeSpecificAnnotations(property.property, renderer)]; + annotations.push(...getTypeSpecificAnnotations(property.property, renderer)); return renderer.renderBlock([...annotations, content]); } @@ -43,15 +43,27 @@ function getTypeSpecificAnnotations(property: ConstrainedMetaModel, renderer: Cl function getStringAnnotations(property: ConstrainedStringModel, renderer: ClassRenderer): string[] { const annotations: string[] = []; const originalInput = property.originalInput; - const pattern = originalInput['pattern']; - if (pattern !== undefined) { - annotations.push(renderer.renderAnnotation('Pattern', { regexp: `"${pattern}"` }, 'get:')); + + if (originalInput['pattern'] !== undefined) { + annotations.push( + renderer.renderAnnotation( + 'Pattern', + { regexp: `"${originalInput['pattern']}"` }, + 'get:' + ) + ); } - const minLength = originalInput['minLength']; - const maxLength = originalInput['maxLength']; - if (minLength !== undefined || maxLength !== undefined) { - annotations.push(renderer.renderAnnotation('Size', { min: minLength, max: maxLength }, 'get:')); + + if (originalInput['minLength'] !== undefined || originalInput['maxLength'] !== undefined) { + annotations.push( + renderer.renderAnnotation( + 'Size', + { min: originalInput['minLength'], max: originalInput['maxLength'] }, + 'get:' + ) + ); } + return annotations; } @@ -59,21 +71,25 @@ function getNumericAnnotations(property: ConstrainedIntegerModel | ConstrainedFl const annotations: string[] = []; const originalInput = property.originalInput; - const minimum = originalInput['minimum']; - if (minimum !== undefined) { - annotations.push(renderer.renderAnnotation('Min', minimum, 'get:')); + if (originalInput['minimum'] !== undefined) { + annotations.push(renderer.renderAnnotation('Min', originalInput['minimum'], 'get:')); } - const exclusiveMinimum = originalInput['exclusiveMinimum']; - if (exclusiveMinimum !== undefined) { - annotations.push(renderer.renderAnnotation('Min', exclusiveMinimum + 1), 'get:'); + + if (originalInput['exclusiveMinimum'] !== undefined) { + annotations.push(renderer.renderAnnotation('Min', originalInput['exclusiveMinimum'] + 1), 'get:'); } - const maximum = originalInput['maximum']; - if (maximum !== undefined) { - annotations.push(renderer.renderAnnotation('Max', maximum, 'get:')); + + if (originalInput['maximum'] !== undefined) { + annotations.push(renderer.renderAnnotation('Max', originalInput['maximum'], 'get:')); } - const exclusiveMaximum = originalInput['exclusiveMaximum']; - if (exclusiveMaximum !== undefined) { - annotations.push(renderer.renderAnnotation('Max', exclusiveMaximum - 1, 'get:')); + + if (originalInput['exclusiveMaximum'] !== undefined) { + annotations.push( + renderer.renderAnnotation( + 'Max', + originalInput['exclusiveMaximum'] - 1, + 'get:') + ); } return annotations; @@ -83,10 +99,14 @@ function getArrayAnnotations(property: ConstrainedArrayModel, renderer: ClassRen const annotations: string[] = []; const originalInput = property.originalInput; - const minItems = originalInput['minItems']; - const maxItems = originalInput['maxItems']; - if (minItems !== undefined || maxItems !== undefined) { - annotations.push(renderer.renderAnnotation('Size', { min: minItems, max: maxItems }, 'get:')); + if (originalInput['minItems'] !== undefined || originalInput['maxItems'] !== undefined) { + annotations.push( + renderer.renderAnnotation( + 'Size', + { min: originalInput['minItems'], max: originalInput['maxItems'] }, + 'get:' + ) + ); } return annotations; From c29b3e602b3704bd691750b2ac40cfc620879740 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 10:55:43 +0100 Subject: [PATCH 25/45] refactor --- src/generators/kotlin/presets/DescriptionPreset.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/generators/kotlin/presets/DescriptionPreset.ts b/src/generators/kotlin/presets/DescriptionPreset.ts index beb4b98e92..843e63f0d5 100644 --- a/src/generators/kotlin/presets/DescriptionPreset.ts +++ b/src/generators/kotlin/presets/DescriptionPreset.ts @@ -7,13 +7,11 @@ function renderDescription({ renderer, content, item }: { content: string, item: (ConstrainedObjectModel | ConstrainedEnumModel) }): string { - const desc = item.originalInput['description']; - - if (!desc) { + if (!item.originalInput['description']) { return content; } - let comment = `${desc}`; + let comment = `${item.originalInput['description']}`; if (item instanceof ConstrainedObjectModel) { const properties = Object.keys(item.properties) From 8e2230932ba52d28e5af15615133940c97ca6601 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 11:01:05 +0100 Subject: [PATCH 26/45] refactor --- src/generators/kotlin/KotlinConstrainer.ts | 2 +- src/generators/kotlin/KotlinRenderer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/generators/kotlin/KotlinConstrainer.ts b/src/generators/kotlin/KotlinConstrainer.ts index 22dfd458e1..9e69b659be 100644 --- a/src/generators/kotlin/KotlinConstrainer.ts +++ b/src/generators/kotlin/KotlinConstrainer.ts @@ -126,7 +126,7 @@ export const KotlinDefaultTypeMapping: TypeMapping = { return uniqueTypes.length > 1 ? interpretUnionValueType(uniqueTypes) : uniqueTypes[0]; }, Union (): string { - // No Unions in Kotlin, use Any + // No Unions in Kotlin, use Any for now. return 'Any'; }, Dictionary ({ constrainedModel }): string { diff --git a/src/generators/kotlin/KotlinRenderer.ts b/src/generators/kotlin/KotlinRenderer.ts index a6d9ab9995..b7708c0031 100644 --- a/src/generators/kotlin/KotlinRenderer.ts +++ b/src/generators/kotlin/KotlinRenderer.ts @@ -38,7 +38,7 @@ ${newLiteral} return `${name}(${value})`; } - const entries = Object.entries(value || {}) + const entries = Object.entries(value || {}); if (entries.length === 0) { return name; From eaf3c464a9aad7b6743351ea2f9a81d9cca6fc6b Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 11:16:34 +0100 Subject: [PATCH 27/45] update test name --- examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap | 2 +- examples/kotlin-generate-kdoc/index.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap b/examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap index e808a45f3f..1bb0756987 100644 --- a/examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap +++ b/examples/kotlin-generate-kdoc/__snapshots__/index.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Should be able to generate JavaDocs and should log expected output to console 1`] = ` +exports[`Should be able to generate KDoc and should log expected output to console 1`] = ` Array [ "/** * Description for class diff --git a/examples/kotlin-generate-kdoc/index.spec.ts b/examples/kotlin-generate-kdoc/index.spec.ts index 00a88bf611..38c6b5b04c 100644 --- a/examples/kotlin-generate-kdoc/index.spec.ts +++ b/examples/kotlin-generate-kdoc/index.spec.ts @@ -1,7 +1,7 @@ const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; }); import {generate} from './index'; -describe('Should be able to generate JavaDocs', () => { +describe('Should be able to generate KDoc', () => { afterAll(() => { jest.restoreAllMocks(); }); From 6301515e4e4875c7f4e8ea56bc9fe5a387af12ad Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 11:22:53 +0100 Subject: [PATCH 28/45] format --- src/generators/kotlin/KotlinRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/kotlin/KotlinRenderer.ts b/src/generators/kotlin/KotlinRenderer.ts index b7708c0031..f2a60fb403 100644 --- a/src/generators/kotlin/KotlinRenderer.ts +++ b/src/generators/kotlin/KotlinRenderer.ts @@ -1,7 +1,7 @@ import { AbstractRenderer } from '../AbstractRenderer'; import { KotlinGenerator, KotlinOptions } from './KotlinGenerator'; import { ConstrainedMetaModel, InputMetaModel, Preset } from '../../models'; -import {FormatHelpers} from '../../helpers'; +import { FormatHelpers } from '../../helpers'; /** * Common renderer for Kotlin From b793664e8bee16ea99686ad93cb262196bc8ac18 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 11:40:20 +0100 Subject: [PATCH 29/45] doc & new example --- docs/languages/Kotlin.md | 35 ++++++++++--------- .../kotlin-change-collection-type/README.md | 17 +++++++++ .../index.spec.ts | 13 +++++++ .../kotlin-change-collection-type/index.ts | 30 ++++++++++++++++ .../package-lock.json | 10 ++++++ .../package.json | 10 ++++++ 6 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 examples/kotlin-change-collection-type/README.md create mode 100644 examples/kotlin-change-collection-type/index.spec.ts create mode 100644 examples/kotlin-change-collection-type/index.ts create mode 100644 examples/kotlin-change-collection-type/package-lock.json create mode 100644 examples/kotlin-change-collection-type/package.json diff --git a/docs/languages/Kotlin.md b/docs/languages/Kotlin.md index 8f36048ef1..565bd14a8e 100644 --- a/docs/languages/Kotlin.md +++ b/docs/languages/Kotlin.md @@ -1,10 +1,13 @@ -# Kotlin WIP +# Kotlin There are special use-cases that each language supports; this document pertains to **Kotlin models**. Since `data classes` are used for every model that has got properties, there is no need for additional settings or features to generate `toString()`, `equals()`, `hashCode()`, getters or setters. +Classes without properties are depicted by usual `classes`, they get no `toString()`, `equals()` or `hashCode()` +implementation. The default one should suffice here. + @@ -13,32 +16,32 @@ features to generate `toString()`, `equals()`, `hashCode()`, getters or setters - [Change the collection type for arrays](#change-the-collection-type-for-arrays) - [Include Javax validation constraint annotations for properties](#include-javax-validation-constraint-annotations-for-properties) - [Generate serializer and deserializer functionality](#generate-serializer-and-deserializer-functionality) - * [To and from JSON](#to-and-from-json) - * [To and from XML](#to-and-from-xml) - * [To and from binary](#to-and-from-binary) + ## Include KDoc for properties +To generate models containing `KDoc` from description and examples, use the `KOTLIN_DESCRIPTION_PRESET` option. + +Check out this [example for a live demonstration](../../examples/kotlin-generate-kdoc). ## Change the collection type for arrays -## Include Javax validation constraint annotations for properties +Sometimes, we might want to render a different collection type, and instead of the default `Array` use as `List` type. To do so, provide the option `collectionType: 'List'`. -In some cases, when you generate the models from JSON Schema, you may want to include `javax.validation.constraint` annotations. +Check out this [example for a live demonstration](../../examples/kotlin-change-collection-type). -## Generate serializer and deserializer functionality +## Include Javax validation constraint annotations for properties -The most widely used usecase for Modelina is to generate models that include serialization and deserialization functionality to convert the models into payload data. This payload data can of course be many different kinds, JSON, XML, raw binary, you name it. +In some cases, when you generate the models from JSON Schema, you may want to include `javax.validation.constraint` annotations. -As you normally only need one library to do this, we developers can never get enough with creating new stuff, therefore there might be one specific library you need or want to integrate with. Therefore there is not one specific preset that offers everything. Below is a list of all the supported serialization presets. +Check out this [example for a live demonstration](../../examples/kotlin-generate-javax-constraint-annotation). -### To and from JSON -Partly supported, since say Jackson-annotations are usually not needed on Kotlin data classes. -If a need was to arise, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! +## Generate serializer and deserializer functionality -### To and from XML -Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! +The most widely used usecase for Modelina is to generate models that include serialization and deserialization functionality to convert the models into payload data. This payload data can of course be many kinds, JSON, XML, raw binary, you name it. -### To and from binary -Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! +As you normally only need one library to do this, we developers can never get enough with creating new stuff, therefore there might be one specific library you need or want to integrate with. Therefore, there is not one specific preset that offers everything. Below is a list of all the supported serialization presets. +As Kotlin integration with the common parsers and mappers, like Jackson, is pretty good, there are no special presets +included as of now. If you need annotations or presets to have more control over your JSON, XML and/or Binary mapping, +[let everybody know](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md). diff --git a/examples/kotlin-change-collection-type/README.md b/examples/kotlin-change-collection-type/README.md new file mode 100644 index 0000000000..72a30068a6 --- /dev/null +++ b/examples/kotlin-change-collection-type/README.md @@ -0,0 +1,17 @@ +# Kotlin change collection type + +A basic example to render collections as Array type in Kotlin. + +## How to run this example + +Run this example using: + +```sh +npm i && npm run start +``` + +If you are on Windows, use the `start:windows` script instead: + +```sh +npm i && npm run start:windows +``` diff --git a/examples/kotlin-change-collection-type/index.spec.ts b/examples/kotlin-change-collection-type/index.spec.ts new file mode 100644 index 0000000000..e244a43a31 --- /dev/null +++ b/examples/kotlin-change-collection-type/index.spec.ts @@ -0,0 +1,13 @@ +const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; }); +import {generate} from './index'; + +describe('Should be able to render collections in Kotlin as Array', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + test('and should log expected output to console', async () => { + await generate(); + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + }); +}); diff --git a/examples/kotlin-change-collection-type/index.ts b/examples/kotlin-change-collection-type/index.ts new file mode 100644 index 0000000000..e03da89f2d --- /dev/null +++ b/examples/kotlin-change-collection-type/index.ts @@ -0,0 +1,30 @@ +import { KotlinGenerator } from '../../src'; + +const generator = new KotlinGenerator({ + collectionType: 'Array' +}); +const jsonSchemaDraft7 = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + additionalProperties: false, + properties: { + email: { + type: 'array', + additionalItems: false, + items: { + type: 'string', + format: 'email' + } + } + } +}; + +export async function generate() : Promise { + const models = await generator.generate(jsonSchemaDraft7); + for (const model of models) { + console.log(model.result); + } +} +if (require.main === module) { + generate(); +} diff --git a/examples/kotlin-change-collection-type/package-lock.json b/examples/kotlin-change-collection-type/package-lock.json new file mode 100644 index 0000000000..5d453bc6a9 --- /dev/null +++ b/examples/kotlin-change-collection-type/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "kotlin-change-collection-type", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/kotlin-change-collection-type/package.json b/examples/kotlin-change-collection-type/package.json new file mode 100644 index 0000000000..7fab9e8213 --- /dev/null +++ b/examples/kotlin-change-collection-type/package.json @@ -0,0 +1,10 @@ +{ + "config" : { "example_name" : "kotlin-change-collection-type" }, + "scripts": { + "install": "cd ../.. && npm i", + "start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts", + "start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts", + "test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts", + "test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts" + } +} From 02014743fdaadbe6a8a868f6430f47555cc5fba4 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 15:46:32 +0100 Subject: [PATCH 30/45] review findings 1 --- .../__snapshots__/index.spec.ts.snap | 9 +++++ output | 35 ------------------- test/blackbox/blackbox-kotlin.spec.ts | 4 +-- 3 files changed, 11 insertions(+), 37 deletions(-) create mode 100644 examples/kotlin-change-collection-type/__snapshots__/index.spec.ts.snap delete mode 100644 output diff --git a/examples/kotlin-change-collection-type/__snapshots__/index.spec.ts.snap b/examples/kotlin-change-collection-type/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..987cdd0fbb --- /dev/null +++ b/examples/kotlin-change-collection-type/__snapshots__/index.spec.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to render collections in Kotlin as Array and should log expected output to console 1`] = ` +Array [ + "data class Root( + val email: Array, +)", +] +`; diff --git a/output b/output deleted file mode 100644 index 37884a89b3..0000000000 --- a/output +++ /dev/null @@ -1,35 +0,0 @@ - - - public class Root { - private Email email; - - public Email getEmail() { return this.email; } - public void setEmail(Email email) { this.email = email; } -} -public enum Email { - EXAMPLE1_AT_TEST_DOT_COM((String)"example1@test.com"), EXAMPLE2_AT_TEST_DOT_COM((String)"example2@test.com"); - - private String value; - - Email(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - public static Email fromValue(String value) { - for (Email e : Email.values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unexpected value '" + value + "'"); - } - - @Override - public String toString() { - return String.valueOf(value); - } -} \ No newline at end of file diff --git a/test/blackbox/blackbox-kotlin.spec.ts b/test/blackbox/blackbox-kotlin.spec.ts index f437a1a14d..7578095940 100644 --- a/test/blackbox/blackbox-kotlin.spec.ts +++ b/test/blackbox/blackbox-kotlin.spec.ts @@ -41,7 +41,7 @@ describe.each(filesToTest)('Should be able to generate with inputs', ({ file, ou }); describe(file, () => { - const javaGeneratorOptions = [ + const kotlinGeneratorOptions = [ { generatorOption: {}, description: 'default generator', @@ -58,7 +58,7 @@ describe.each(filesToTest)('Should be able to generate with inputs', ({ file, ou } ]; - describe.each(javaGeneratorOptions)('should be able to generate and compile Kotlin', ({ generatorOption, renderOutputPath }) => { + describe.each(kotlinGeneratorOptions)('should be able to generate and compile Kotlin', ({ generatorOption, renderOutputPath }) => { test('class and enums', async () => { const generator = new KotlinFileGenerator(generatorOption); From 163e94cc0cc485686244dcd7fa878be9d6326666 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 15:52:15 +0100 Subject: [PATCH 31/45] add examples to readme --- examples/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index bc8344604a..f9977e9393 100644 --- a/examples/README.md +++ b/examples/README.md @@ -22,6 +22,7 @@ We love contributions and new examples that does not already exist, you can foll - [Java](#java) - [C#](#c%23) - [TypeScript](#typescript) +- [Kotlin](#kotlin) - [Other examples](#other-examples) @@ -58,6 +59,7 @@ These are all the basic generator examples that shows a bare minimal example of - [generate-java-models](./generate-java-models) - A basic example to generate Java data models. - [generate-go-models](./generate-go-models) - A basic example to generate Go data models - [generate-javascript-models](./generate-javascript-models) - A basic example to generate JavaScript data models +- [generate-kotlin-models](./generate-kotlin-models) - A basic example to generate Kotlin data models ## Integrations These are examples of how you can integrate Modelina into a specific scenario: @@ -107,6 +109,13 @@ These are all specific examples only relevant to the TypeScript generator: - [typescript-use-cjs](./typescript-use-cjs) - A basic example that generate the models to use CJS module system. - [typescript-generate-jsonbinpack](./typescript-generate-jsonbinpack) - A basic example showing how to generate models that include [jsonbinpack](https://github.com/sourcemeta/jsonbinpack) functionality. +## Kotlin +These are all specific examples only relevant to the Kotlin generator: +- [generate-kotlin-enums](./generate-kotlin-enums) +- [kotlin-generate-kdoc](./kotlin-generate-kdoc) +- [kotlin-generate-javax-constraint-annotations](./kotlin-generate-javax-constraint-annotation) +- [kotlin-change-collection-type](./kotlin-change-collection-type) + ## Other examples Miscelanious examples that do not fit into the otherwise grouping. -- [TEMPLATE](./TEMPLATE) - A basic template used to create new examples. \ No newline at end of file +- [TEMPLATE](./TEMPLATE) - A basic template used to create new examples. From c4a23a8289e19e93c9d46a615560a046a52c711a Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 16:02:27 +0100 Subject: [PATCH 32/45] add docs about missing serde support --- docs/languages/Kotlin.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/languages/Kotlin.md b/docs/languages/Kotlin.md index 565bd14a8e..57fcbc66b0 100644 --- a/docs/languages/Kotlin.md +++ b/docs/languages/Kotlin.md @@ -16,7 +16,9 @@ implementation. The default one should suffice here. - [Change the collection type for arrays](#change-the-collection-type-for-arrays) - [Include Javax validation constraint annotations for properties](#include-javax-validation-constraint-annotations-for-properties) - [Generate serializer and deserializer functionality](#generate-serializer-and-deserializer-functionality) - + * [To and from JSON](#to-and-from-json) + * [To and from XML](#to-and-from-xml) + * [To and from binary](#to-and-from-binary) ## Include KDoc for properties @@ -42,6 +44,9 @@ The most widely used usecase for Modelina is to generate models that include ser As you normally only need one library to do this, we developers can never get enough with creating new stuff, therefore there might be one specific library you need or want to integrate with. Therefore, there is not one specific preset that offers everything. Below is a list of all the supported serialization presets. -As Kotlin integration with the common parsers and mappers, like Jackson, is pretty good, there are no special presets -included as of now. If you need annotations or presets to have more control over your JSON, XML and/or Binary mapping, -[let everybody know](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md). +### To and from JSON +Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! +### To and from XML +Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! +### To and from binary +Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)! From 7b61833514e0bb4f7eecec399bff08028e741050 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 16:04:33 +0100 Subject: [PATCH 33/45] fix wanky import --- src/generators/kotlin/KotlinConstrainer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/kotlin/KotlinConstrainer.ts b/src/generators/kotlin/KotlinConstrainer.ts index 9e69b659be..189cdb1807 100644 --- a/src/generators/kotlin/KotlinConstrainer.ts +++ b/src/generators/kotlin/KotlinConstrainer.ts @@ -1,4 +1,4 @@ -import { ConstrainedEnumValueModel } from 'models'; +import { ConstrainedEnumValueModel } from '../../models'; import { TypeMapping } from '../../helpers'; import { defaultEnumKeyConstraints, defaultEnumValueConstraints } from './constrainer/EnumConstrainer'; import { defaultModelNameConstraints } from './constrainer/ModelNameConstrainer'; From fc14e890e4e344c60e5320756f60be9b77825bc7 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 16:05:36 +0100 Subject: [PATCH 34/45] adapt usage.md --- docs/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index a718d5cae5..46287e9521 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -174,4 +174,4 @@ Python is one of the many output languages we support. Check out this [basic exa ## Generate Kotlin models -Kotlin is one of the many output languages we support. Check out this [basic example for a live demonstration](../examples/generate-kotlin-models) and the following [Kotlin documentation for more advanced use-cases](./languages/Kotlin.md). +Kotlin is one of the many output languages we support. Check out this [basic example for a live demonstration](../examples/generate-kotlin-models) as well as [how to generate enums](../examples/generate-kotlin-enums) and the following [Kotlin documentation for more advanced use-cases](./languages/Kotlin.md). From 6fca2e0fbfc7f5f60b2b8aabb6b73871f3052087 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 16:09:51 +0100 Subject: [PATCH 35/45] add note about JDK --- test/blackbox/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/blackbox/README.md b/test/blackbox/README.md index fe43ea2608..8aaee551de 100644 --- a/test/blackbox/README.md +++ b/test/blackbox/README.md @@ -27,7 +27,7 @@ If you want to run the BlackBox tests locally, you have to install a couple of d - To run the `Go` BlackBox tests, you need to have GoLang installed - https://golang.org/doc/install - To run the `Python` BlackBox tests, you need to have python installed - https://www.python.org/downloads/ - To run the `Rust` BlackBox tests, you need to have rust installed - https://www.rust-lang.org/tools/install (if you are on mac you might also need to install xcode `xcode-select --install`) -- To run the `Kotlin` BlackBox tests, you need to have Kotlin installed - https://kotlinlang.org/docs/command-line.html +- To run the `Kotlin` BlackBox tests, you need to have a JDK as well as kotlinc installed - https://kotlinlang.org/docs/command-line.html By default, the BlackBox tests are not run with the regular `npm run test`, but can be run with `npm run test:blackbox`. Or run individual BlackBox tests you can run the commands `npm run test:blackbox:${language}` where language is one of `csharp`, `go`, `java`, `javascript`, `python`, `rust`, `typescript`, etc. From ebb096d2355bf0ee90632c3b4f186f7ab4b9754c Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 16:10:40 +0100 Subject: [PATCH 36/45] specify which jdk --- test/blackbox/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/blackbox/README.md b/test/blackbox/README.md index 8aaee551de..fa3dce39bb 100644 --- a/test/blackbox/README.md +++ b/test/blackbox/README.md @@ -27,7 +27,7 @@ If you want to run the BlackBox tests locally, you have to install a couple of d - To run the `Go` BlackBox tests, you need to have GoLang installed - https://golang.org/doc/install - To run the `Python` BlackBox tests, you need to have python installed - https://www.python.org/downloads/ - To run the `Rust` BlackBox tests, you need to have rust installed - https://www.rust-lang.org/tools/install (if you are on mac you might also need to install xcode `xcode-select --install`) -- To run the `Kotlin` BlackBox tests, you need to have a JDK as well as kotlinc installed - https://kotlinlang.org/docs/command-line.html +- To run the `Kotlin` BlackBox tests, you need to have a JDK >= 8 as well as kotlinc installed - https://kotlinlang.org/docs/command-line.html By default, the BlackBox tests are not run with the regular `npm run test`, but can be run with `npm run test:blackbox`. Or run individual BlackBox tests you can run the commands `npm run test:blackbox:${language}` where language is one of `csharp`, `go`, `java`, `javascript`, `python`, `rust`, `typescript`, etc. From 8ce1853725aea1195e9559f7e5461ffe7ae3291c Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Fri, 13 Jan 2023 16:19:25 +0100 Subject: [PATCH 37/45] ignore output --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f89e1b2643..3cc80de7e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,5 @@ node_modules coverage lib *.DS_Store -.vscode .idea +output From d6a5863ac2047b2de536674382c10ef9254826ea Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Mon, 16 Jan 2023 10:42:13 +0100 Subject: [PATCH 38/45] failing blackbox test for constraints --- Dockerfile | 6 ++++-- test/blackbox/blackbox-kotlin.spec.ts | 11 ++++++----- .../kotlin/validation-api-2.0.1.Final.jar | Bin 0 -> 93107 bytes 3 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 test/blackbox/dependencies/kotlin/validation-api-2.0.1.Final.jar diff --git a/Dockerfile b/Dockerfile index ba9f38fbff..8d6d164ca5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update -yq \ # Install nodejs RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ - && apt-get install -yq nodejs + && apt-get install -yq nodejs # Install golang RUN curl -fsSL https://golang.org/dl/go1.16.8.linux-amd64.tar.gz | tar -C /usr/local -xz @@ -17,7 +17,7 @@ RUN apt install apt-transport-https dirmngr gnupg ca-certificates -yq \ && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF \ && echo "deb https://download.mono-project.com/repo/debian stable-buster main" | tee /etc/apt/sources.list.d/mono-official-stable.list \ && apt update -yq \ - && apt install mono-devel -yq + && apt install mono-devel -yq # Install rust RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y @@ -25,6 +25,8 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y # Install Python RUN apt-get install -yq python +# TODO install kotlin before completing PR + # Setup library RUN apt-get install -yq chromium diff --git a/test/blackbox/blackbox-kotlin.spec.ts b/test/blackbox/blackbox-kotlin.spec.ts index 7578095940..7dea96e2b6 100644 --- a/test/blackbox/blackbox-kotlin.spec.ts +++ b/test/blackbox/blackbox-kotlin.spec.ts @@ -11,7 +11,7 @@ import { InputMetaModel, InputProcessor, KotlinFileGenerator, - KOTLIN_DEFAULT_PRESET, + KOTLIN_DEFAULT_PRESET, JAVA_COMMON_PRESET, KOTLIN_CONSTRAINTS_PRESET, } from '../../src'; import { execCommand } from './utils/Utils'; import filesToTest from './BlackBoxTestFiles'; @@ -50,22 +50,23 @@ describe.each(filesToTest)('Should be able to generate with inputs', ({ file, ou { generatorOption: { presets: [ - KOTLIN_DEFAULT_PRESET + KOTLIN_CONSTRAINTS_PRESET ] }, - description: 'all common presets', - renderOutputPath: path.resolve(outputDirectoryPath, './class/commonpreset') + description: 'constraints preset', + renderOutputPath: path.resolve(outputDirectoryPath, './class/constraints') } ]; describe.each(kotlinGeneratorOptions)('should be able to generate and compile Kotlin', ({ generatorOption, renderOutputPath }) => { test('class and enums', async () => { const generator = new KotlinFileGenerator(generatorOption); + const dependencyPath = path.resolve(__dirname, './dependencies/kotlin/*'); const generatedModels = await generator.generateToFiles(models, renderOutputPath, { packageName: 'main'}); expect(generatedModels).not.toHaveLength(0); - const compileCommand = `kotlinc ${path.resolve(renderOutputPath, '*.kt')} -d ${outputDirectoryPath}`; + const compileCommand = `kotlinc -cp ${dependencyPath} ${path.resolve(renderOutputPath, '*.kt')} -d ${renderOutputPath}`; await execCommand(compileCommand); }); }); diff --git a/test/blackbox/dependencies/kotlin/validation-api-2.0.1.Final.jar b/test/blackbox/dependencies/kotlin/validation-api-2.0.1.Final.jar new file mode 100644 index 0000000000000000000000000000000000000000..2368e10a5323ad6bae79f6cfac89d61e66b15f37 GIT binary patch literal 93107 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

pY$L{s_ZSzlwTZidEm3>v{$mF$KL1%b)ndT# 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%0D7w0g;lZDXXggTSJ4z73oVpY=&sM2;OOcLmyBF$k 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 V;XsI~z<bz@8E6{{ePDU>E=Z literal 0 HcmV?d00001 From 61ac2c19c6f90835e77b9e75c692a4d18e56ef86 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 17 Jan 2023 17:06:48 +0100 Subject: [PATCH 39/45] fix constraint preset --- .../__snapshots__/index.spec.ts.snap | 2 ++ .../kotlin-generate-javax-constraint-annotation/index.ts | 1 + src/generators/kotlin/presets/ConstraintsPreset.ts | 7 ++++++- test/blackbox/blackbox-kotlin.spec.ts | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap b/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap index bc955b5483..1337db614d 100644 --- a/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap +++ b/examples/kotlin-generate-javax-constraint-annotation/__snapshots__/index.spec.ts.snap @@ -9,6 +9,8 @@ Array [ @get:NotNull @get:Max(99) val maxNumberProp: Double, + @get:Min(101) + val minNumberPropExclusive: Double, @get:Size(min=2, max=3) val arrayProp: List, @get:Pattern(regexp=\\"^I_\\") diff --git a/examples/kotlin-generate-javax-constraint-annotation/index.ts b/examples/kotlin-generate-javax-constraint-annotation/index.ts index 5456a5cd50..71eb769ed1 100644 --- a/examples/kotlin-generate-javax-constraint-annotation/index.ts +++ b/examples/kotlin-generate-javax-constraint-annotation/index.ts @@ -10,6 +10,7 @@ const jsonSchemaDraft7 = { properties: { min_number_prop: { type: 'number', minimum: 0 }, max_number_prop: { type: 'number', exclusiveMaximum: 100 }, + min_number_prop_exclusive: { type: 'number', exclusiveMinimum: 100 }, array_prop: { type: 'array', minItems: 2, maxItems: 3, }, string_prop: { type: 'string', pattern: '^I_', minLength: 3 } }, diff --git a/src/generators/kotlin/presets/ConstraintsPreset.ts b/src/generators/kotlin/presets/ConstraintsPreset.ts index 6d44480bcd..ab4ed43227 100644 --- a/src/generators/kotlin/presets/ConstraintsPreset.ts +++ b/src/generators/kotlin/presets/ConstraintsPreset.ts @@ -76,7 +76,12 @@ function getNumericAnnotations(property: ConstrainedIntegerModel | ConstrainedFl } if (originalInput['exclusiveMinimum'] !== undefined) { - annotations.push(renderer.renderAnnotation('Min', originalInput['exclusiveMinimum'] + 1), 'get:'); + annotations.push( + renderer.renderAnnotation( + 'Min', + originalInput['exclusiveMinimum'] + 1, + 'get:') + ); } if (originalInput['maximum'] !== undefined) { diff --git a/test/blackbox/blackbox-kotlin.spec.ts b/test/blackbox/blackbox-kotlin.spec.ts index 7dea96e2b6..325fa60c3a 100644 --- a/test/blackbox/blackbox-kotlin.spec.ts +++ b/test/blackbox/blackbox-kotlin.spec.ts @@ -66,7 +66,7 @@ describe.each(filesToTest)('Should be able to generate with inputs', ({ file, ou const generatedModels = await generator.generateToFiles(models, renderOutputPath, { packageName: 'main'}); expect(generatedModels).not.toHaveLength(0); - const compileCommand = `kotlinc -cp ${dependencyPath} ${path.resolve(renderOutputPath, '*.kt')} -d ${renderOutputPath}`; + const compileCommand = `kotlinc ${path.resolve(renderOutputPath, '*.kt')} -cp ${dependencyPath} -d ${renderOutputPath}`; await execCommand(compileCommand); }); }); From a5c562c52c5819c2ef2536326bbe666c7f7cca63 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 17 Jan 2023 17:07:52 +0100 Subject: [PATCH 40/45] add myself to CODEOWNERS --- CODEOWNERS | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0dfaa382a4..ce4a305a3b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -7,23 +7,23 @@ # The default owners are automatically added as reviewers when you open a pull request unless different owners are specified in the file. # Core Champions that does a little of everything -* @magicmatatjahu @jonaslagoni @asyncapi-bot-eve +* @magicmatatjahu @jonaslagoni @asyncapi-bot-eve # Documentation champions /docs # Input Champions for AsyncAPI input -*/processors/AsyncAPIInputProcessor*.ts +*/processors/AsyncAPIInputProcessor*.ts # Input Champions for TypeScript input */processors/TypeScriptInputProcessor*.ts @ron-debajyoti # Input Champions for OpenAPI input -*/processors/OpenAPIInputProcessor*.ts -*/processors/SwaggerInputProcessor*.ts +*/processors/OpenAPIInputProcessor*.ts +*/processors/SwaggerInputProcessor*.ts # Input Champions for JSON Schema input -*/processors/JsonSchemaInputProcessor*.ts +*/processors/JsonSchemaInputProcessor*.ts # Language Champions for TypeScript and it's presets */generators/typescript @Samridhi-98 @@ -46,3 +46,5 @@ # Language Champions for Rust and its presets */generators/rust @leigh-johnson +# Language Champions for Kotlin and it's presets +*/generators/kotlin @LouisXhaferi From cb886f49f60d58d224ee4b9bee0dd8ba932ba947 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 17 Jan 2023 18:04:03 +0100 Subject: [PATCH 41/45] ignore windows blackbox tests --- test/blackbox/blackbox-kotlin.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/blackbox/blackbox-kotlin.spec.ts b/test/blackbox/blackbox-kotlin.spec.ts index 325fa60c3a..ace79b9724 100644 --- a/test/blackbox/blackbox-kotlin.spec.ts +++ b/test/blackbox/blackbox-kotlin.spec.ts @@ -30,6 +30,13 @@ function deleteDirectoryIfExists(directory: string) { } describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + const isWindows = process.platform === 'win32'; + if (isWindows) { + // Windows environment has a weird setup, where it is using Kotlin Native instead of Kotlin JVM as it's compiler + // (We'll link an issue here, once we know why this is the case) + return; + } + jest.setTimeout(1000000); const fileToGenerateFor = path.resolve(__dirname, file); const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'kotlin'); From 716ef54019c511e55cbbc20ebb77c3751e794078 Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 17 Jan 2023 19:00:00 +0100 Subject: [PATCH 42/45] add kotlin to dockerfile --- Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8d6d164ca5..10e5b9a616 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,13 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y # Install Python RUN apt-get install -yq python -# TODO install kotlin before completing PR +# Install Kotlin +RUN apt install -yq wget unzip \ + && cd /usr/lib \ + && wget -q https://github.com/JetBrains/kotlin/releases/download/v1.8.0/kotlin-compiler-1.8.0.zip \ + && unzip -qq kotlin-compiler-*.zip + +ENV PATH $PATH:/usr/lib/kotlinc/bin # Setup library RUN apt-get install -yq chromium From 673c72d48947f90cef74aaf87b1db05faff299fe Mon Sep 17 00:00:00 2001 From: Louis Xhaferi Date: Tue, 17 Jan 2023 19:45:20 +0100 Subject: [PATCH 43/45] link issue --- test/blackbox/blackbox-kotlin.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/blackbox/blackbox-kotlin.spec.ts b/test/blackbox/blackbox-kotlin.spec.ts index ace79b9724..703a3a84f9 100644 --- a/test/blackbox/blackbox-kotlin.spec.ts +++ b/test/blackbox/blackbox-kotlin.spec.ts @@ -33,7 +33,7 @@ describe.each(filesToTest)('Should be able to generate with inputs', ({ file, ou const isWindows = process.platform === 'win32'; if (isWindows) { // Windows environment has a weird setup, where it is using Kotlin Native instead of Kotlin JVM as it's compiler - // (We'll link an issue here, once we know why this is the case) + // (See https://github.com/asyncapi/modelina/issues/1080) return; } From 2cae07c444fd6c3141de66c61da47176a686b63d Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Wed, 18 Jan 2023 10:28:21 +0100 Subject: [PATCH 44/45] Fixed blackbox tests was not getting ignored in Windows --- test/blackbox/blackbox-kotlin.spec.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/blackbox/blackbox-kotlin.spec.ts b/test/blackbox/blackbox-kotlin.spec.ts index 703a3a84f9..7868d7c034 100644 --- a/test/blackbox/blackbox-kotlin.spec.ts +++ b/test/blackbox/blackbox-kotlin.spec.ts @@ -29,13 +29,12 @@ function deleteDirectoryIfExists(directory: string) { } } -describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { - const isWindows = process.platform === 'win32'; - if (isWindows) { - // Windows environment has a weird setup, where it is using Kotlin Native instead of Kotlin JVM as it's compiler - // (See https://github.com/asyncapi/modelina/issues/1080) - return; - } +const isWindows = process.platform === 'win32'; +const describeIf = (condition: boolean) => condition ? describe : describe.skip; + +// Windows environment has a weird setup, where it is using Kotlin Native instead of Kotlin JVM as it's compiler +// (See https://github.com/asyncapi/modelina/issues/1080) +describeIf(!isWindows).each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { jest.setTimeout(1000000); const fileToGenerateFor = path.resolve(__dirname, file); From c76b6d8c0d3b055d6c104c4cf560dcdc06c96e32 Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Wed, 18 Jan 2023 10:47:57 +0100 Subject: [PATCH 45/45] Fix linting for blackbox-kotlin --- test/blackbox/blackbox-kotlin.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/blackbox/blackbox-kotlin.spec.ts b/test/blackbox/blackbox-kotlin.spec.ts index 7868d7c034..5fa23018ab 100644 --- a/test/blackbox/blackbox-kotlin.spec.ts +++ b/test/blackbox/blackbox-kotlin.spec.ts @@ -35,7 +35,6 @@ const describeIf = (condition: boolean) => condition ? describe : describe.skip; // Windows environment has a weird setup, where it is using Kotlin Native instead of Kotlin JVM as it's compiler // (See https://github.com/asyncapi/modelina/issues/1080) describeIf(!isWindows).each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { - jest.setTimeout(1000000); const fileToGenerateFor = path.resolve(__dirname, file); const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'kotlin');