From 5f74d7accc21ffbab9f1a32d630832648ac7c8f0 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Sat, 16 Dec 2023 14:19:11 +0200 Subject: [PATCH 1/8] Adding Scala generator scaffolding --- src/generators/scala/Constants.ts | 14 + src/generators/scala/ScalaConstrainer.ts | 62 +++++ .../scala/ScalaDependencyManager.ts | 11 + src/generators/scala/ScalaFileGenerator.ts | 45 ++++ src/generators/scala/ScalaGenerator.ts | 245 ++++++++++++++++++ src/generators/scala/ScalaPreset.ts | 24 ++ src/generators/scala/ScalaRenderer.ts | 28 ++ .../scala/constrainer/ConstantConstrainer.ts | 7 + .../scala/constrainer/EnumConstrainer.ts | 105 ++++++++ .../scala/constrainer/ModelNameConstrainer.ts | 53 ++++ .../constrainer/PropertyKeyConstrainer.ts | 80 ++++++ src/generators/scala/index.ts | 21 ++ .../scala/presets/DescriptionPreset.ts | 25 ++ src/generators/scala/presets/index.ts | 1 + .../scala/renderers/ClassRenderer.ts | 95 +++++++ .../scala/renderers/EnumRenderer.ts | 82 ++++++ 16 files changed, 898 insertions(+) create mode 100644 src/generators/scala/Constants.ts create mode 100644 src/generators/scala/ScalaConstrainer.ts create mode 100644 src/generators/scala/ScalaDependencyManager.ts create mode 100644 src/generators/scala/ScalaFileGenerator.ts create mode 100644 src/generators/scala/ScalaGenerator.ts create mode 100644 src/generators/scala/ScalaPreset.ts create mode 100644 src/generators/scala/ScalaRenderer.ts create mode 100644 src/generators/scala/constrainer/ConstantConstrainer.ts create mode 100644 src/generators/scala/constrainer/EnumConstrainer.ts create mode 100644 src/generators/scala/constrainer/ModelNameConstrainer.ts create mode 100644 src/generators/scala/constrainer/PropertyKeyConstrainer.ts create mode 100644 src/generators/scala/index.ts create mode 100644 src/generators/scala/presets/DescriptionPreset.ts create mode 100644 src/generators/scala/presets/index.ts create mode 100644 src/generators/scala/renderers/ClassRenderer.ts create mode 100644 src/generators/scala/renderers/EnumRenderer.ts diff --git a/src/generators/scala/Constants.ts b/src/generators/scala/Constants.ts new file mode 100644 index 0000000000..f10a996f44 --- /dev/null +++ b/src/generators/scala/Constants.ts @@ -0,0 +1,14 @@ +import { checkForReservedKeyword } from '../../helpers'; + +export const RESERVED_SCALA_KEYWORDS = ['abstract', 'continue']; + +export function isReservedScalaKeyword( + word: string, + forceLowerCase = true +): boolean { + return checkForReservedKeyword( + word, + RESERVED_SCALA_KEYWORDS, + forceLowerCase + ); +} diff --git a/src/generators/scala/ScalaConstrainer.ts b/src/generators/scala/ScalaConstrainer.ts new file mode 100644 index 0000000000..e739f2d4bc --- /dev/null +++ b/src/generators/scala/ScalaConstrainer.ts @@ -0,0 +1,62 @@ +import { Constraints, TypeMapping } from '../../helpers'; +import { + defaultEnumKeyConstraints, + defaultEnumValueConstraints +} from './constrainer/EnumConstrainer'; +import { defaultModelNameConstraints } from './constrainer/ModelNameConstrainer'; +import { defaultPropertyKeyConstraints } from './constrainer/PropertyKeyConstrainer'; +import { defaultConstantConstraints } from './constrainer/ConstantConstrainer'; +import { ScalaDependencyManager } from './ScalaDependencyManager'; +import { ScalaOptions } from './ScalaGenerator'; + +export const ScalaDefaultTypeMapping: TypeMapping< + ScalaOptions, + ScalaDependencyManager +> = { + Object({ constrainedModel }): string { + //Returning name here because all object models have been split out + return constrainedModel.name; + }, + Reference({ constrainedModel }): string { + return constrainedModel.name; + }, + Any(): string { + return ''; + }, + Float(): string { + return ''; + }, + Integer(): string { + return ''; + }, + String(): string { + return ''; + }, + Boolean(): string { + return ''; + }, + Tuple(): string { + return ''; + }, + Array(): string { + return ''; + }, + Enum({ constrainedModel }): string { + //Returning name here because all enum models have been split out + return constrainedModel.name; + }, + Union(): string { + return ''; + }, + Dictionary(): string { + return ''; + } +}; + +export const ScalaDefaultConstraints: Constraints = { + enumKey: defaultEnumKeyConstraints(), + enumValue: defaultEnumValueConstraints(), + modelName: defaultModelNameConstraints(), + propertyKey: defaultPropertyKeyConstraints(), + constant: defaultConstantConstraints() +}; diff --git a/src/generators/scala/ScalaDependencyManager.ts b/src/generators/scala/ScalaDependencyManager.ts new file mode 100644 index 0000000000..339de058c8 --- /dev/null +++ b/src/generators/scala/ScalaDependencyManager.ts @@ -0,0 +1,11 @@ +import { AbstractDependencyManager } from '../AbstractDependencyManager'; +import { ScalaOptions } from './ScalaGenerator'; + +export class ScalaDependencyManager extends AbstractDependencyManager { + constructor( + public options: ScalaOptions, + dependencies: string[] = [] + ) { + super(dependencies); + } +} diff --git a/src/generators/scala/ScalaFileGenerator.ts b/src/generators/scala/ScalaFileGenerator.ts new file mode 100644 index 0000000000..0fa5ddc51d --- /dev/null +++ b/src/generators/scala/ScalaFileGenerator.ts @@ -0,0 +1,45 @@ +import { ScalaGenerator, ScalaRenderCompleteModelOptions } from '.'; +import { InputMetaModel, OutputModel } from '../../models'; +import * as path from 'path'; +import { AbstractFileGenerator } from '../AbstractFileGenerator'; +import { FileHelpers } from '../../helpers'; + +export class ScalaFileGenerator + extends ScalaGenerator + 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?: ScalaRenderCompleteModelOptions, + 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}.MYEXTENSION` + ); + await FileHelpers.writerToFileSystem( + outputModel.result, + filePath, + ensureFilesWritten + ); + } + return generatedModels; + } +} diff --git a/src/generators/scala/ScalaGenerator.ts b/src/generators/scala/ScalaGenerator.ts new file mode 100644 index 0000000000..64b13a5b15 --- /dev/null +++ b/src/generators/scala/ScalaGenerator.ts @@ -0,0 +1,245 @@ +import { + AbstractGenerator, + AbstractGeneratorRenderArgs, + AbstractGeneratorRenderCompleteModelArgs, + CommonGeneratorOptions, + defaultGeneratorOptions +} from '../AbstractGenerator'; +import { + ConstrainedEnumModel, + ConstrainedMetaModel, + ConstrainedObjectModel, + InputMetaModel, + MetaModel, + RenderOutput +} from '../../models'; +import { split, TypeMapping } from '../../helpers'; +import { ScalaPreset, SCALA_DEFAULT_PRESET } from './ScalaPreset'; +import { ClassRenderer } from './renderers/ClassRenderer'; +import { EnumRenderer } from './renderers/EnumRenderer'; +import { isReservedScalaKeyword } from './Constants'; +import { Logger } from '../..'; +import { + constrainMetaModel, + Constraints +} from '../../helpers/ConstrainHelpers'; +import { + ScalaDefaultConstraints, + ScalaDefaultTypeMapping +} from './ScalaConstrainer'; +import { DeepPartial, mergePartialAndDefault } from '../../utils/Partials'; +import { ScalaDependencyManager } from './ScalaDependencyManager'; + +export interface ScalaOptions + extends CommonGeneratorOptions { + typeMapping: TypeMapping; + constraints: Constraints; +} +export interface ScalaRenderCompleteModelOptions { + packageName: string; +} +export class ScalaGenerator extends AbstractGenerator< + ScalaOptions, + ScalaRenderCompleteModelOptions +> { + static defaultOptions: ScalaOptions = { + ...defaultGeneratorOptions, + defaultPreset: SCALA_DEFAULT_PRESET, + typeMapping: ScalaDefaultTypeMapping, + constraints: ScalaDefaultConstraints + }; + + static defaultCompleteModelOptions: ScalaRenderCompleteModelOptions = { + packageName: 'Asyncapi.Models' + }; + + constructor(options?: DeepPartial) { + const realizedOptions = ScalaGenerator.getScalaOptions(options); + super('Scala', realizedOptions); + } + + /** + * Returns the Scala options by merging custom options with default ones. + */ + static getScalaOptions( + options?: DeepPartial + ): ScalaOptions { + const optionsToUse = mergePartialAndDefault( + ScalaGenerator.defaultOptions, + options + ) as ScalaOptions; + //Always overwrite the dependency manager unless user explicitly state they want it (ignore default temporary dependency manager) + if (options?.dependencyManager === undefined) { + optionsToUse.dependencyManager = () => { + return new ScalaDependencyManager(optionsToUse); + }; + } + return optionsToUse; + } + + /** + * Wrapper to get an instance of the dependency manager + */ + getDependencyManager(options: ScalaOptions): ScalaDependencyManager { + return this.getDependencyManagerInstance( + options + ) as ScalaDependencyManager; + } + + /** + * This function makes sure we split up the MetaModels accordingly to what we want to render as models. + */ + splitMetaModel(model: MetaModel): MetaModel[] { + //These are the models that we have separate renderers for + const metaModelsToSplit = { + splitEnum: true, + splitObject: true + }; + return split(model, metaModelsToSplit); + } + + constrainToMetaModel( + model: MetaModel, + options: DeepPartial + ): ConstrainedMetaModel { + const optionsToUse = ScalaGenerator.getScalaOptions({ + ...this.options, + ...options + }); + const dependencyManagerToUse = this.getDependencyManager(optionsToUse); + return constrainMetaModel( + this.options.typeMapping, + this.options.constraints, + { + metaModel: model, + dependencyManager: dependencyManagerToUse, + 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( + args: AbstractGeneratorRenderArgs + ): Promise { + if (args.constrainedModel instanceof ConstrainedObjectModel) { + return this.renderClass(args.constrainedModel, args.inputModel); + } else if (args.constrainedModel instanceof ConstrainedEnumModel) { + return this.renderEnum(args.constrainedModel, args.inputModel); + } + Logger.warn( + `Scala generator, cannot generate this type of model, ${args.constrainedModel.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 Scala 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( + args: AbstractGeneratorRenderCompleteModelArgs< + ScalaOptions, + ScalaRenderCompleteModelOptions + > + ): Promise { + const completeModelOptionsToUse = + mergePartialAndDefault( + ScalaGenerator.defaultCompleteModelOptions, + args.completeOptions + ); + + if (isReservedScalaKeyword(completeModelOptionsToUse.packageName)) { + throw new Error( + `You cannot use reserved Scala keyword (${args.completeOptions.packageName}) as package name, please use another.` + ); + } + + const outputModel = await this.render(args); + const modelDependencies = args.constrainedModel + .getNearestDependencies() + .map((dependencyModel) => { + return `import ${completeModelOptionsToUse.packageName}.${dependencyModel.name};`; + }); + const outputContent = `package ${completeModelOptionsToUse.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, + options?: Partial + ): Promise { + const optionsToUse = ScalaGenerator.getScalaOptions({ + ...this.options, + ...options + }); + const dependencyManagerToUse = this.getDependencyManager(optionsToUse); + const presets = this.getPresets('class'); + const renderer = new ClassRenderer( + this.options, + this, + presets, + model, + inputModel, + dependencyManagerToUse + ); + const result = await renderer.runSelfPreset(); + return RenderOutput.toRenderOutput({ + result, + renderedName: model.name, + dependencies: dependencyManagerToUse.dependencies + }); + } + + async renderEnum( + model: ConstrainedEnumModel, + inputModel: InputMetaModel, + options?: Partial + ): Promise { + const optionsToUse = ScalaGenerator.getScalaOptions({ + ...this.options, + ...options + }); + const dependencyManagerToUse = this.getDependencyManager(optionsToUse); + const presets = this.getPresets('enum'); + const renderer = new EnumRenderer( + this.options, + this, + presets, + model, + inputModel, + dependencyManagerToUse + ); + const result = await renderer.runSelfPreset(); + return RenderOutput.toRenderOutput({ + result, + renderedName: model.name, + dependencies: dependencyManagerToUse.dependencies + }); + } +} diff --git a/src/generators/scala/ScalaPreset.ts b/src/generators/scala/ScalaPreset.ts new file mode 100644 index 0000000000..19388a4fdd --- /dev/null +++ b/src/generators/scala/ScalaPreset.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { Preset, ClassPreset, EnumPreset } from '../../models'; +import { ScalaOptions } from './ScalaGenerator'; +import { + ClassRenderer, + SCALA_DEFAULT_CLASS_PRESET +} from './renderers/ClassRenderer'; +import { + EnumRenderer, + SCALA_DEFAULT_ENUM_PRESET +} from './renderers/EnumRenderer'; + +export type ClassPresetType = ClassPreset; +export type EnumPresetType = EnumPreset; + +export type ScalaPreset = Preset<{ + class: ClassPresetType; + enum: EnumPresetType; +}>; + +export const SCALA_DEFAULT_PRESET: ScalaPreset = { + class: SCALA_DEFAULT_CLASS_PRESET, + enum: SCALA_DEFAULT_ENUM_PRESET +}; diff --git a/src/generators/scala/ScalaRenderer.ts b/src/generators/scala/ScalaRenderer.ts new file mode 100644 index 0000000000..2ce8f1ebd2 --- /dev/null +++ b/src/generators/scala/ScalaRenderer.ts @@ -0,0 +1,28 @@ +import { AbstractRenderer } from '../AbstractRenderer'; +import { ScalaGenerator, ScalaOptions } from './ScalaGenerator'; +import { ConstrainedMetaModel, InputMetaModel, Preset } from '../../models'; +import { ScalaDependencyManager } from './ScalaDependencyManager'; + +/** + * Common renderer for Scala + * + * @extends AbstractRenderer + */ +export abstract class ScalaRenderer< + RendererModelType extends ConstrainedMetaModel +> extends AbstractRenderer< + ScalaOptions, + ScalaGenerator, + RendererModelType +> { + constructor( + options: ScalaOptions, + generator: ScalaGenerator, + presets: Array<[Preset, unknown]>, + model: RendererModelType, + inputModel: InputMetaModel, + public dependencyManager: ScalaDependencyManager + ) { + super(options, generator, presets, model, inputModel); + } +} diff --git a/src/generators/scala/constrainer/ConstantConstrainer.ts b/src/generators/scala/constrainer/ConstantConstrainer.ts new file mode 100644 index 0000000000..2c076ad46d --- /dev/null +++ b/src/generators/scala/constrainer/ConstantConstrainer.ts @@ -0,0 +1,7 @@ +import { ConstantConstraint } from '../../../helpers'; + +export function defaultConstantConstraints(): ConstantConstraint { + return () => { + return undefined; + }; +} diff --git a/src/generators/scala/constrainer/EnumConstrainer.ts b/src/generators/scala/constrainer/EnumConstrainer.ts new file mode 100644 index 0000000000..9ba2e2cb83 --- /dev/null +++ b/src/generators/scala/constrainer/EnumConstrainer.ts @@ -0,0 +1,105 @@ +/* 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 { isReservedScalaKeyword } 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 it gets formatted by NAMING_FORMATTER + //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, isReservedScalaKeyword); + } +}; + +/** + * Default constraint logic for Scala, which converts the enum key into a key that is compatible with Scala + */ +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; + }; +} + +/** + * Convert the enum value to a value that is compatible with Scala + */ +export function defaultEnumValueConstraints(): EnumValueConstraint { + return ({ enumValue }) => { + let constrainedEnumValue = enumValue; + switch (typeof enumValue) { + case 'string': + case 'boolean': + constrainedEnumValue = `"${enumValue}"`; + break; + case 'bigint': + case 'number': { + constrainedEnumValue = enumValue; + break; + } + case 'object': { + constrainedEnumValue = `"${JSON.stringify(enumValue).replace( + /"/g, + '\\"' + )}"`; + break; + } + default: { + constrainedEnumValue = `"${JSON.stringify(enumValue)}"`; + } + } + return constrainedEnumValue; + }; +} diff --git a/src/generators/scala/constrainer/ModelNameConstrainer.ts b/src/generators/scala/constrainer/ModelNameConstrainer.ts new file mode 100644 index 0000000000..c9c523dff2 --- /dev/null +++ b/src/generators/scala/constrainer/ModelNameConstrainer.ts @@ -0,0 +1,53 @@ +import { + NO_NUMBER_START_CHAR, + NO_EMPTY_VALUE, + NO_RESERVED_KEYWORDS +} from '../../../helpers/Constraints'; +import { FormatHelpers, ModelNameConstraint } from '../../../helpers'; +import { isReservedScalaKeyword } 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, isReservedScalaKeyword); + } +}; + +/** + * Default constraint logic for Scala, which converts the model name into something that is compatible with Scala + */ +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/scala/constrainer/PropertyKeyConstrainer.ts b/src/generators/scala/constrainer/PropertyKeyConstrainer.ts new file mode 100644 index 0000000000..640761546c --- /dev/null +++ b/src/generators/scala/constrainer/PropertyKeyConstrainer.ts @@ -0,0 +1,80 @@ +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 { isReservedScalaKeyword } 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, isReservedScalaKeyword); + } +}; +/** + * Default constraint logic for Scala, which converts the object property key into something that is compatible with Scala + */ +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/scala/index.ts b/src/generators/scala/index.ts new file mode 100644 index 0000000000..d538f6c328 --- /dev/null +++ b/src/generators/scala/index.ts @@ -0,0 +1,21 @@ +export * from './ScalaGenerator'; +export * from './ScalaFileGenerator'; +export { SCALA_DEFAULT_PRESET } from './ScalaPreset'; +export type { ScalaPreset } from './ScalaPreset'; +export * from './presets'; + +export { + defaultEnumKeyConstraints as scalaDefaultEnumKeyConstraints, + DefaultEnumKeyConstraints as ScalaDefaultEnumKeyConstraints, + defaultEnumValueConstraints as scalaDefaultEnumValueConstraints +} from './constrainer/EnumConstrainer'; + +export { + DefaultModelNameConstraints as ScalaDefaultModelNameConstraints, + defaultModelNameConstraints as scalaDefaultModelNameConstraints +} from './constrainer/ModelNameConstrainer'; + +export { + DefaultPropertyKeyConstraints as ScalaDefaultPropertyKeyConstraints, + defaultPropertyKeyConstraints as scalaDefaultPropertyKeyConstraints +} from './constrainer/PropertyKeyConstrainer'; diff --git a/src/generators/scala/presets/DescriptionPreset.ts b/src/generators/scala/presets/DescriptionPreset.ts new file mode 100644 index 0000000000..6169ab68c4 --- /dev/null +++ b/src/generators/scala/presets/DescriptionPreset.ts @@ -0,0 +1,25 @@ +import { ScalaPreset } from '../ScalaPreset'; + +/** + * Preset which adds description to rendered model. + * + * @implements {ScalaPreset} + */ +export const SCALA_DESCRIPTION_PRESET: ScalaPreset = { + 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/scala/presets/index.ts b/src/generators/scala/presets/index.ts new file mode 100644 index 0000000000..e74c57b288 --- /dev/null +++ b/src/generators/scala/presets/index.ts @@ -0,0 +1 @@ +export * from './DescriptionPreset'; diff --git a/src/generators/scala/renderers/ClassRenderer.ts b/src/generators/scala/renderers/ClassRenderer.ts new file mode 100644 index 0000000000..f9d2b45884 --- /dev/null +++ b/src/generators/scala/renderers/ClassRenderer.ts @@ -0,0 +1,95 @@ +import { ScalaRenderer } from '../ScalaRenderer'; +import { + ConstrainedDictionaryModel, + ConstrainedObjectModel, + ConstrainedObjectPropertyModel +} from '../../../models'; +import { FormatHelpers } from '../../../helpers'; +import { ScalaOptions } from '../ScalaGenerator'; +import { ClassPresetType } from '../ScalaPreset'; + +/** + * Renderer for Scala's `class` type + * + * @extends ScalaRenderer + */ +export class ClassRenderer extends ScalaRenderer { + 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 SCALA_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/scala/renderers/EnumRenderer.ts b/src/generators/scala/renderers/EnumRenderer.ts new file mode 100644 index 0000000000..3d09fe4294 --- /dev/null +++ b/src/generators/scala/renderers/EnumRenderer.ts @@ -0,0 +1,82 @@ +import { ScalaRenderer } from '../ScalaRenderer'; +import { + ConstrainedEnumModel, + ConstrainedEnumValueModel +} from '../../../models'; +import { EnumPresetType } from '../ScalaPreset'; +import { ScalaOptions } from '../ScalaGenerator'; + +/** + * Renderer for Scala's `enum` type + * + * @extends ScalaRenderer + */ +export class EnumRenderer extends ScalaRenderer { + async defaultSelf(): Promise { + const content = [ + await this.renderItems(), + await this.runAdditionalContentPreset() + ]; + return `public enum ${this.model.name} { +${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(', '); + return `${content};`; + } + + runItemPreset(item: ConstrainedEnumValueModel): Promise { + return this.runPreset('item', { item }); + } +} + +export const SCALA_DEFAULT_ENUM_PRESET: EnumPresetType = { + self({ renderer }) { + renderer.dependencyManager.addDependency( + 'import com.fasterxml.jackson.annotation.*;' + ); + return renderer.defaultSelf(); + }, + item({ item }) { + return `${item.key}(${item.value})`; + }, + additionalContent({ model }) { + const enumValueType = 'Object'; + + return `private ${enumValueType} value; + +${model.type}(${enumValueType} value) { + this.value = value; +} + +@JsonValue +public ${enumValueType} getValue() { + return value; +} + +@Override +public String toString() { + return String.valueOf(value); +} + +@JsonCreator +public static ${model.type} fromValue(${enumValueType} value) { + for (${model.type} e : ${model.type}.values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); +}`; + } +}; From 34b3cd6b7eb7c635897c02e37462c8fa1490c249 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Sat, 16 Dec 2023 15:33:52 +0200 Subject: [PATCH 2/8] Adding Scala specific generator code --- src/generators/scala/Constants.ts | 42 ++++- src/generators/scala/ScalaConstrainer.ts | 144 +++++++++++++++--- .../scala/ScalaDependencyManager.ts | 9 ++ src/generators/scala/ScalaFileGenerator.ts | 10 +- src/generators/scala/ScalaGenerator.ts | 74 +++++---- .../scala/renderers/ClassRenderer.ts | 74 +++------ .../scala/renderers/EnumRenderer.ts | 54 ++----- 7 files changed, 259 insertions(+), 148 deletions(-) diff --git a/src/generators/scala/Constants.ts b/src/generators/scala/Constants.ts index f10a996f44..5ad81ff5e5 100644 --- a/src/generators/scala/Constants.ts +++ b/src/generators/scala/Constants.ts @@ -1,6 +1,46 @@ import { checkForReservedKeyword } from '../../helpers'; -export const RESERVED_SCALA_KEYWORDS = ['abstract', 'continue']; +export const RESERVED_SCALA_KEYWORDS = [ + 'abstract', + 'case', + 'catch', + 'class', + 'def', + 'do', + 'else', + 'extends', + 'false', + 'final', + 'finally', + 'for', + 'forSome', + 'if', + 'implicit', + 'import', + 'lazy', + 'match', + 'new', + 'null', + 'object', + 'override', + 'package', + 'private', + 'protected', + 'return', + 'sealed', + 'super', + 'this', + 'throw', + 'trait', + 'true', + 'try', + 'type', + 'val', + 'var', + 'while', + 'with', + 'yield', +]; export function isReservedScalaKeyword( word: string, diff --git a/src/generators/scala/ScalaConstrainer.ts b/src/generators/scala/ScalaConstrainer.ts index e739f2d4bc..f004c279ec 100644 --- a/src/generators/scala/ScalaConstrainer.ts +++ b/src/generators/scala/ScalaConstrainer.ts @@ -1,4 +1,5 @@ -import { Constraints, TypeMapping } from '../../helpers'; +import { Constraints } from '../../helpers'; +import { ConstrainedEnumValueModel } from '../../models'; import { defaultEnumKeyConstraints, defaultEnumValueConstraints @@ -6,50 +7,143 @@ import { import { defaultModelNameConstraints } from './constrainer/ModelNameConstrainer'; import { defaultPropertyKeyConstraints } from './constrainer/PropertyKeyConstrainer'; import { defaultConstantConstraints } from './constrainer/ConstantConstrainer'; -import { ScalaDependencyManager } from './ScalaDependencyManager'; -import { ScalaOptions } from './ScalaGenerator'; +import { ScalaTypeMapping } from './ScalaGenerator'; -export const ScalaDefaultTypeMapping: TypeMapping< - ScalaOptions, - ScalaDependencyManager -> = { +function enumFormatToNumberType( + enumValueModel: ConstrainedEnumValueModel, + format: string | undefined +): 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 | undefined +): 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 ScalaDefaultTypeMapping: ScalaTypeMapping = { Object({ constrainedModel }): string { - //Returning name here because all object models have been split out return constrainedModel.name; }, Reference({ constrainedModel }): string { return constrainedModel.name; }, Any(): string { - return ''; + return 'Any'; }, - Float(): string { - return ''; + Float({ constrainedModel }): string { + return constrainedModel.options.format === 'float' ? 'Float' : 'Double'; }, - Integer(): string { - return ''; + Integer({ constrainedModel }): string { + return constrainedModel.options.format === 'long' || + constrainedModel.options.format === 'int64' + ? 'Long' + : 'Int'; }, - String(): string { - return ''; + String({ constrainedModel }): string { + switch (constrainedModel.options.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 'Array[Byte]'; + } + default: { + return 'String'; + } + } }, Boolean(): string { - return ''; + return 'Boolean'; }, - Tuple(): string { - return ''; + // 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[Any]' : 'Array[Any]'; }, - Array(): string { - return ''; + 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 { - //Returning name here because all enum models have been split out - return constrainedModel.name; + const valueTypes = constrainedModel.values.map((enumValue) => + fromEnumValueToKotlinType(enumValue, constrainedModel.options.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 { - return ''; + // No Unions in Kotlin, use Any for now. + return 'Any'; }, - Dictionary(): string { - return ''; + Dictionary({ constrainedModel }): string { + return `Map[${constrainedModel.key.type}, ${constrainedModel.value.type}]`; } }; diff --git a/src/generators/scala/ScalaDependencyManager.ts b/src/generators/scala/ScalaDependencyManager.ts index 339de058c8..d49d728a54 100644 --- a/src/generators/scala/ScalaDependencyManager.ts +++ b/src/generators/scala/ScalaDependencyManager.ts @@ -8,4 +8,13 @@ export class ScalaDependencyManager extends AbstractDependencyManager { ) { super(dependencies); } + + /** + * Adds a dependency package ensuring correct syntax. + * + * @param dependencyPackage package to import, for example `javax.validation.constraints.*` + */ + addDependency(dependencyPackage: string): void { + super.addDependency(`import ${dependencyPackage}`); + } } diff --git a/src/generators/scala/ScalaFileGenerator.ts b/src/generators/scala/ScalaFileGenerator.ts index 0fa5ddc51d..dab37a9636 100644 --- a/src/generators/scala/ScalaFileGenerator.ts +++ b/src/generators/scala/ScalaFileGenerator.ts @@ -14,17 +14,15 @@ export class ScalaFileGenerator * @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?: ScalaRenderCompleteModelOptions, + options: ScalaRenderCompleteModelOptions, ensureFilesWritten = false ): Promise { - let generatedModels = await this.generateCompleteModels( - input, - options || {} - ); + let generatedModels = await this.generateCompleteModels(input, options); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; @@ -32,7 +30,7 @@ export class ScalaFileGenerator for (const outputModel of generatedModels) { const filePath = path.resolve( outputDirectory, - `${outputModel.modelName}.MYEXTENSION` + `${outputModel.modelName}.scala` ); await FileHelpers.writerToFileSystem( outputModel.result, diff --git a/src/generators/scala/ScalaGenerator.ts b/src/generators/scala/ScalaGenerator.ts index 64b13a5b15..260f9b4dee 100644 --- a/src/generators/scala/ScalaGenerator.ts +++ b/src/generators/scala/ScalaGenerator.ts @@ -34,10 +34,18 @@ export interface ScalaOptions extends CommonGeneratorOptions { typeMapping: TypeMapping; constraints: Constraints; + collectionType: 'List' | 'Array'; } + +export type ScalaTypeMapping = TypeMapping< + ScalaOptions, + ScalaDependencyManager +> + export interface ScalaRenderCompleteModelOptions { packageName: string; } + export class ScalaGenerator extends AbstractGenerator< ScalaOptions, ScalaRenderCompleteModelOptions @@ -45,14 +53,11 @@ export class ScalaGenerator extends AbstractGenerator< static defaultOptions: ScalaOptions = { ...defaultGeneratorOptions, defaultPreset: SCALA_DEFAULT_PRESET, + collectionType: 'List', typeMapping: ScalaDefaultTypeMapping, constraints: ScalaDefaultConstraints }; - static defaultCompleteModelOptions: ScalaRenderCompleteModelOptions = { - packageName: 'Asyncapi.Models' - }; - constructor(options?: DeepPartial) { const realizedOptions = ScalaGenerator.getScalaOptions(options); super('Scala', realizedOptions); @@ -90,7 +95,6 @@ export class ScalaGenerator extends AbstractGenerator< * This function makes sure we split up the MetaModels accordingly to what we want to render as models. */ splitMetaModel(model: MetaModel): MetaModel[] { - //These are the models that we have separate renderers for const metaModelsToSplit = { splitEnum: true, splitObject: true @@ -128,10 +132,22 @@ export class ScalaGenerator extends AbstractGenerator< render( args: AbstractGeneratorRenderArgs ): Promise { + const optionsToUse = ScalaGenerator.getScalaOptions({ + ...this.options, + ...args.options + }); if (args.constrainedModel instanceof ConstrainedObjectModel) { - return this.renderClass(args.constrainedModel, args.inputModel); + return this.renderClass( + args.constrainedModel, + args.inputModel, + optionsToUse + ); } else if (args.constrainedModel instanceof ConstrainedEnumModel) { - return this.renderEnum(args.constrainedModel, args.inputModel); + return this.renderEnum( + args.constrainedModel, + args.inputModel, + optionsToUse + ); } Logger.warn( `Scala generator, cannot generate this type of model, ${args.constrainedModel.name}` @@ -160,27 +176,20 @@ export class ScalaGenerator extends AbstractGenerator< ScalaRenderCompleteModelOptions > ): Promise { - const completeModelOptionsToUse = - mergePartialAndDefault( - ScalaGenerator.defaultCompleteModelOptions, - args.completeOptions - ); - - if (isReservedScalaKeyword(completeModelOptionsToUse.packageName)) { - throw new Error( - `You cannot use reserved Scala keyword (${args.completeOptions.packageName}) as package name, please use another.` - ); - } - - const outputModel = await this.render(args); - const modelDependencies = args.constrainedModel - .getNearestDependencies() - .map((dependencyModel) => { - return `import ${completeModelOptionsToUse.packageName}.${dependencyModel.name};`; - }); - const outputContent = `package ${completeModelOptionsToUse.packageName}; -${modelDependencies.join('\n')} + const optionsToUse = ScalaGenerator.getScalaOptions({ + ...this.options, + ...args.options + }); + const outputModel = await this.render({ + ...args, + options: optionsToUse + }); + const packageName = this.sanitizePackageName( + args.completeOptions.packageName ?? 'Asyncapi.Models' + ); + const outputContent = `package ${packageName} ${outputModel.dependencies.join('\n')} + ${outputModel.result}`; return RenderOutput.toRenderOutput({ result: outputContent, @@ -189,6 +198,17 @@ ${outputModel.result}`; }); } + private sanitizePackageName(packageName: string): string { + return packageName + .split('.') + .map((subpackage) => + isReservedScalaKeyword(subpackage, true) + ? `\`${subpackage}\`` + : subpackage + ) + .join('.'); + } + async renderClass( model: ConstrainedObjectModel, inputModel: InputMetaModel, diff --git a/src/generators/scala/renderers/ClassRenderer.ts b/src/generators/scala/renderers/ClassRenderer.ts index f9d2b45884..90eb56f61f 100644 --- a/src/generators/scala/renderers/ClassRenderer.ts +++ b/src/generators/scala/renderers/ClassRenderer.ts @@ -1,39 +1,41 @@ import { ScalaRenderer } from '../ScalaRenderer'; import { - ConstrainedDictionaryModel, ConstrainedObjectModel, ConstrainedObjectPropertyModel } from '../../../models'; -import { FormatHelpers } from '../../../helpers'; import { ScalaOptions } from '../ScalaGenerator'; import { ClassPresetType } from '../ScalaPreset'; +function getPropertyType(property: ConstrainedObjectPropertyModel): string { + const propertyType = property.required ? + property.property.type : `Option[${property.property.type}]`; + + return propertyType; +} + /** * Renderer for Scala's `class` type * * @extends ScalaRenderer */ export class ClassRenderer extends ScalaRenderer { - 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.runCtorPreset(), - await this.renderAccessors(), await this.runAdditionalContentPreset() ]; - return `public class ${this.model.name} { + return `case 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[] = []; @@ -49,47 +51,17 @@ ${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 SCALA_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 `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}; }`; + const propertyType = getPropertyType(property); + + return `${property.propertyName}: ${propertyType},`; } }; diff --git a/src/generators/scala/renderers/EnumRenderer.ts b/src/generators/scala/renderers/EnumRenderer.ts index 3d09fe4294..9ef92c70a0 100644 --- a/src/generators/scala/renderers/EnumRenderer.ts +++ b/src/generators/scala/renderers/EnumRenderer.ts @@ -5,6 +5,7 @@ import { } from '../../../models'; import { EnumPresetType } from '../ScalaPreset'; import { ScalaOptions } from '../ScalaGenerator'; +import { FormatHelpers } from 'helpers'; /** * Renderer for Scala's `enum` type @@ -12,12 +13,15 @@ import { ScalaOptions } from '../ScalaGenerator'; * @extends ScalaRenderer */ export class EnumRenderer extends ScalaRenderer { - async defaultSelf(): Promise { + async defaultSelf(valueType: string): Promise { const content = [ await this.renderItems(), + await this.runFromValuePreset(), await this.runAdditionalContentPreset() ]; - return `public enum ${this.model.name} { + return `object ${this.model.name} extends Enumeration { + type ${this.model.name} = Value + ${this.indent(this.renderBlock(content, 2))} }`; } @@ -31,52 +35,26 @@ ${this.indent(this.renderBlock(content, 2))} items.push(renderedItem); } - const content = items.join(', '); + const content = items.join('\n'); return `${content};`; } runItemPreset(item: ConstrainedEnumValueModel): Promise { return this.runPreset('item', { item }); } + + runFromValuePreset(): Promise { + return this.runPreset('fromValue'); + } } -export const SCALA_DEFAULT_ENUM_PRESET: EnumPresetType = { - self({ renderer }) { - renderer.dependencyManager.addDependency( - 'import com.fasterxml.jackson.annotation.*;' - ); - return renderer.defaultSelf(); +export const SCALA_DEFAULT_ENUM_PRESET: EnumPresetType = { + self({ renderer, model }) { + return renderer.defaultSelf(model.type); }, item({ item }) { - return `${item.key}(${item.value})`; - }, - additionalContent({ model }) { - const enumValueType = 'Object'; - - return `private ${enumValueType} value; - -${model.type}(${enumValueType} value) { - this.value = value; -} + const key = FormatHelpers.toPascalCase(item.key); -@JsonValue -public ${enumValueType} getValue() { - return value; -} - -@Override -public String toString() { - return String.valueOf(value); -} - -@JsonCreator -public static ${model.type} fromValue(${enumValueType} value) { - for (${model.type} e : ${model.type}.values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unexpected value '" + value + "'"); -}`; + return `val ${key} = Value("${item.value}")` } }; From fee1ff565f99900fa0381730acd7563740f0addb Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Sat, 16 Dec 2023 15:36:46 +0200 Subject: [PATCH 3/8] Use ScalaOptions instead of KotlinOptions --- src/generators/scala/renderers/EnumRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/scala/renderers/EnumRenderer.ts b/src/generators/scala/renderers/EnumRenderer.ts index 9ef92c70a0..825bb68f73 100644 --- a/src/generators/scala/renderers/EnumRenderer.ts +++ b/src/generators/scala/renderers/EnumRenderer.ts @@ -48,7 +48,7 @@ ${this.indent(this.renderBlock(content, 2))} } } -export const SCALA_DEFAULT_ENUM_PRESET: EnumPresetType = { +export const SCALA_DEFAULT_ENUM_PRESET: EnumPresetType = { self({ renderer, model }) { return renderer.defaultSelf(model.type); }, From 5550d71cd6c44acb1a861238f01f7c57191d2403 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Sat, 16 Dec 2023 16:21:46 +0200 Subject: [PATCH 4/8] Adding tests --- src/generators/scala/ScalaRenderer.ts | 51 +- .../scala/presets/DescriptionPreset.ts | 58 +- test/TestUtils/TestRenderers.ts | 2 + test/generators/scala/Constants.spec.ts | 12 + .../generators/scala/ScalaConstrainer.spec.ts | 512 ++++++++++++++++++ test/generators/scala/ScalaGenerator.spec.ts | 211 ++++++++ test/generators/scala/ScalaRenderer.spec.ts | 52 ++ .../scala/presets/DescriptionPreset.spec.ts | 44 ++ 8 files changed, 927 insertions(+), 15 deletions(-) create mode 100644 test/generators/scala/Constants.spec.ts create mode 100644 test/generators/scala/ScalaConstrainer.spec.ts create mode 100644 test/generators/scala/ScalaGenerator.spec.ts create mode 100644 test/generators/scala/ScalaRenderer.spec.ts create mode 100644 test/generators/scala/presets/DescriptionPreset.spec.ts diff --git a/src/generators/scala/ScalaRenderer.ts b/src/generators/scala/ScalaRenderer.ts index 2ce8f1ebd2..c5120264bd 100644 --- a/src/generators/scala/ScalaRenderer.ts +++ b/src/generators/scala/ScalaRenderer.ts @@ -1,6 +1,7 @@ import { AbstractRenderer } from '../AbstractRenderer'; import { ScalaGenerator, ScalaOptions } from './ScalaGenerator'; import { ConstrainedMetaModel, InputMetaModel, Preset } from '../../models'; +import { FormatHelpers } from '../../helpers'; import { ScalaDependencyManager } from './ScalaDependencyManager'; /** @@ -10,11 +11,7 @@ import { ScalaDependencyManager } from './ScalaDependencyManager'; */ export abstract class ScalaRenderer< RendererModelType extends ConstrainedMetaModel -> extends AbstractRenderer< - ScalaOptions, - ScalaGenerator, - RendererModelType -> { +> extends AbstractRenderer { constructor( options: ScalaOptions, generator: ScalaGenerator, @@ -25,4 +22,48 @@ export abstract class ScalaRenderer< ) { 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} + */`; + } + + renderAnnotation( + annotationName: string, + value?: any | Record + ): string { + const name = `@${annotationName}`; + + if (value === undefined || value === null) { + return name; + } + + if (typeof value !== 'object') { + return `${name}(${value})`; + } + + const entries = Object.entries(value || {}); + + if (entries.length === 0) { + return name; + } + + const values = concatenateEntries(entries); + 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/scala/presets/DescriptionPreset.ts b/src/generators/scala/presets/DescriptionPreset.ts index 6169ab68c4..a7fb1c7ac2 100644 --- a/src/generators/scala/presets/DescriptionPreset.ts +++ b/src/generators/scala/presets/DescriptionPreset.ts @@ -1,4 +1,48 @@ +import { ScalaRenderer } from '../ScalaRenderer'; import { ScalaPreset } from '../ScalaPreset'; +import { FormatHelpers } from '../../../helpers'; +import { ConstrainedEnumModel, ConstrainedObjectModel } from '../../../models'; +function renderDescription({ + renderer, + content, + item +}: { + renderer: ScalaRenderer; + content: string; + item: ConstrainedObjectModel | ConstrainedEnumModel; +}): string { + if (!item.originalInput['description']) { + return content; + } + + let comment = `${item.originalInput['description']}`; + + if (item instanceof ConstrainedObjectModel) { + const properties = Object.keys(item.properties) + .map((key) => item.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. @@ -7,19 +51,13 @@ import { ScalaPreset } from '../ScalaPreset'; */ export const SCALA_DESCRIPTION_PRESET: ScalaPreset = { 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 }); } } }; diff --git a/test/TestUtils/TestRenderers.ts b/test/TestUtils/TestRenderers.ts index 56ca121ff8..063d71bf99 100644 --- a/test/TestUtils/TestRenderers.ts +++ b/test/TestUtils/TestRenderers.ts @@ -15,6 +15,7 @@ import { RustRenderer } from '../../src/generators/rust/RustRenderer'; import { PythonRenderer } from '../../src/generators/python/PythonRenderer'; import { KotlinRenderer } from '../../src/generators/kotlin/KotlinRenderer'; import { PhpRenderer } from '../../src/generators/php/PhpRenderer'; +import { ScalaRenderer } from '../../src/generators/scala/ScalaRenderer'; export class TestRenderer extends AbstractRenderer { constructor(presets = []) { @@ -43,3 +44,4 @@ export class MockRustRenderer extends RustRenderer {} export class MockPythonRenderer extends PythonRenderer {} export class MockKotlinRenderer extends KotlinRenderer {} export class MockPhpRenderer extends PhpRenderer {} +export class MockScalaRenderer extends ScalaRenderer {} diff --git a/test/generators/scala/Constants.spec.ts b/test/generators/scala/Constants.spec.ts new file mode 100644 index 0000000000..59da18ea3e --- /dev/null +++ b/test/generators/scala/Constants.spec.ts @@ -0,0 +1,12 @@ +import { isReservedScalaKeyword } from '../../../src/generators/scala/Constants'; + +describe('Reserved keywords', () => { + it('should return true if the word is a reserved keyword', () => { + expect(isReservedScalaKeyword('abstract')).toBe(true); + expect(isReservedScalaKeyword('type')).toBe(true); + }); + + it('should return false if the word is not a reserved keyword', () => { + expect(isReservedScalaKeyword('dinosaur')).toBe(false); + }); +}); diff --git a/test/generators/scala/ScalaConstrainer.spec.ts b/test/generators/scala/ScalaConstrainer.spec.ts new file mode 100644 index 0000000000..b4c1e411b9 --- /dev/null +++ b/test/generators/scala/ScalaConstrainer.spec.ts @@ -0,0 +1,512 @@ +import { ScalaDefaultTypeMapping } from '../../../src/generators/scala/ScalaConstrainer'; +import { ScalaGenerator, ScalaOptions } from '../../../src/generators/scala'; +import { + ConstrainedAnyModel, + ConstrainedArrayModel, + ConstrainedBooleanModel, + ConstrainedDictionaryModel, + ConstrainedEnumModel, + ConstrainedEnumValueModel, + ConstrainedFloatModel, + ConstrainedIntegerModel, + ConstrainedObjectModel, + ConstrainedReferenceModel, + ConstrainedStringModel, + ConstrainedTupleModel, + ConstrainedTupleValueModel, + ConstrainedUnionModel +} from '../../../src'; +describe('ScalaConstrainer', () => { + describe('ObjectModel', () => { + test('should render the constrained name as type', () => { + const model = new ConstrainedObjectModel('test', undefined, {}, '', {}); + const type = ScalaDefaultTypeMapping.Object({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Reference({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual(model.name); + }); + }); + describe('Any', () => { + test('should render type', () => { + const model = new ConstrainedAnyModel('test', undefined, {}, ''); + const type = ScalaDefaultTypeMapping.Any({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual(''); + }); + }); + describe('Float', () => { + test('should render type', () => { + const model = new ConstrainedFloatModel('test', undefined, {}, ''); + const type = ScalaDefaultTypeMapping.Float({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('Double'); + }); + test('should render Float when format has number format', () => { + const model = new ConstrainedFloatModel( + 'test', + {}, + { format: 'float' }, + '' + ); + const type = ScalaDefaultTypeMapping.Float({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('Float'); + }); + }); + describe('Integer', () => { + test('should render type', () => { + const model = new ConstrainedIntegerModel('test', undefined, {}, ''); + const type = ScalaDefaultTypeMapping.Integer({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('Int'); + }); + test('should render Int when format has integer format', () => { + const model = new ConstrainedIntegerModel( + 'test', + {}, + { format: 'int32' }, + '' + ); + const type = ScalaDefaultTypeMapping.Integer({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('Int'); + }); + test('should render Long when format has long format', () => { + const model = new ConstrainedIntegerModel( + 'test', + {}, + { format: 'long' }, + '' + ); + const type = ScalaDefaultTypeMapping.Integer({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('Long'); + }); + test('should render Long when format has int64 format', () => { + const model = new ConstrainedIntegerModel( + 'test', + {}, + { format: 'int64' }, + '' + ); + const type = ScalaDefaultTypeMapping.Integer({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('Long'); + }); + }); + describe('String', () => { + test('should render type', () => { + const model = new ConstrainedStringModel('test', undefined, {}, ''); + const type = ScalaDefaultTypeMapping.String({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('String'); + }); + test('should render LocalDate when format has date format', () => { + const model = new ConstrainedStringModel( + 'test', + {}, + { format: 'date' }, + '' + ); + const type = ScalaDefaultTypeMapping.String({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('java.time.LocalDate'); + }); + test('should render OffsetTime when format has time format', () => { + const model = new ConstrainedStringModel( + 'test', + {}, + { format: 'time' }, + '' + ); + const type = ScalaDefaultTypeMapping.String({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('java.time.OffsetTime'); + }); + test('should render OffsetDateTime when format has dateTime format', () => { + const model = new ConstrainedStringModel( + 'test', + {}, + { format: 'dateTime' }, + '' + ); + const type = ScalaDefaultTypeMapping.String({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('java.time.OffsetDateTime'); + }); + test('should render OffsetDateTime when format has date-time format', () => { + const model = new ConstrainedStringModel( + 'test', + {}, + { format: 'date-time' }, + '' + ); + const type = ScalaDefaultTypeMapping.String({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('java.time.OffsetDateTime'); + }); + test('should render byte when format has binary format', () => { + const model = new ConstrainedStringModel( + 'test', + {}, + { format: 'binary' }, + '' + ); + const type = ScalaDefaultTypeMapping.String({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('ByteArray'); + }); + }); + + describe('Boolean', () => { + test('should render type', () => { + const model = new ConstrainedBooleanModel('test', undefined, {}, ''); + const type = ScalaDefaultTypeMapping.Boolean({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Tuple({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('List[Any]'); + }); + 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 = ScalaDefaultTypeMapping.Tuple({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('List[Any]'); + }); + }); + + describe('Array', () => { + test('should render type', () => { + const arrayModel = new ConstrainedStringModel( + 'test', + undefined, + {}, + 'String' + ); + const model = new ConstrainedArrayModel( + 'test', + undefined, + {}, + '', + arrayModel + ); + const options: ScalaOptions = { + ...ScalaGenerator.defaultOptions, + collectionType: 'Array' + }; + const type = ScalaDefaultTypeMapping.Array({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('Array[String]'); + }); + test('should render array as a list', () => { + const arrayModel = new ConstrainedStringModel( + 'test', + undefined, + {}, + 'String' + ); + const model = new ConstrainedArrayModel( + 'test', + undefined, + {}, + '', + arrayModel + ); + const options: ScalaOptions = { + ...ScalaGenerator.defaultOptions, + collectionType: 'List' + }; + const type = ScalaDefaultTypeMapping.Array({ + constrainedModel: model, + options, + dependencyManager: undefined as never + }); + expect(type).toEqual('List[String]'); + }); + }); + + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Enum({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Union({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + 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 = ScalaDefaultTypeMapping.Dictionary({ + constrainedModel: model, + options: ScalaGenerator.defaultOptions, + dependencyManager: undefined as never + }); + expect(type).toEqual('Map[String, String]'); + }); + }); +}); diff --git a/test/generators/scala/ScalaGenerator.spec.ts b/test/generators/scala/ScalaGenerator.spec.ts new file mode 100644 index 0000000000..4b3731d042 --- /dev/null +++ b/test/generators/scala/ScalaGenerator.spec.ts @@ -0,0 +1,211 @@ +import { ScalaGenerator } from '../../../src/generators/scala'; + +describe('ScalaGenerator', () => { + let generator: ScalaGenerator; + beforeEach(() => { + generator = new ScalaGenerator(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should not render reserved keyword', async () => { + const doc = { + $id: 'Address', + type: 'object', + properties: { + class: { type: 'string' } + }, + additionalProperties: false + }; + + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); + + 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 + } + } + }; + + generator = new ScalaGenerator({ 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' + } + }, + 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' + } + }, + 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/scala/ScalaRenderer.spec.ts b/test/generators/scala/ScalaRenderer.spec.ts new file mode 100644 index 0000000000..75b85b94d2 --- /dev/null +++ b/test/generators/scala/ScalaRenderer.spec.ts @@ -0,0 +1,52 @@ +import { ScalaGenerator } from '../../../src/generators/scala'; +import { ScalaRenderer } from '../../../src/generators/scala/ScalaRenderer'; +import { ConstrainedObjectModel, InputMetaModel } from '../../../src/models'; +import { MockScalaRenderer } from '../../TestUtils/TestRenderers'; + +describe('ScalaRenderer', () => { + let renderer: ScalaRenderer; + beforeEach(() => { + renderer = new MockScalaRenderer( + ScalaGenerator.defaultOptions, + new ScalaGenerator(), + [], + new ConstrainedObjectModel('', undefined, '', {}), + new InputMetaModel() + ); + }); + + describe('renderComments()', () => { + test('Should be able to render comments', () => { + expect(renderer.renderComments('someComment')).toEqual(`/** + * someComment + */`); + }); + }); + + 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' + ); + }); + }); +}); diff --git a/test/generators/scala/presets/DescriptionPreset.spec.ts b/test/generators/scala/presets/DescriptionPreset.spec.ts new file mode 100644 index 0000000000..6d78deabf0 --- /dev/null +++ b/test/generators/scala/presets/DescriptionPreset.spec.ts @@ -0,0 +1,44 @@ +import { + ScalaGenerator, + SCALA_DESCRIPTION_PRESET +} from '../../../../src/generators/scala'; + +describe('SCALA_DESCRIPTION_PRESET', () => { + let generator: ScalaGenerator; + beforeEach(() => { + generator = new ScalaGenerator({ presets: [SCALA_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(); + }); +}); From 6b05a3673ff393c0dfdef32386f60598af8e1435 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Sat, 16 Dec 2023 22:38:02 +0200 Subject: [PATCH 5/8] Adding more tests and fixing some minor issues --- src/generators/scala/Constants.ts | 8 +- .../scala/ScalaDependencyManager.ts | 5 +- src/generators/scala/ScalaGenerator.ts | 13 +-- .../scala/renderers/ClassRenderer.ts | 5 +- .../scala/renderers/EnumRenderer.ts | 10 +- .../generators/scala/ScalaConstrainer.spec.ts | 6 +- test/generators/scala/ScalaGenerator.spec.ts | 6 +- test/generators/scala/ScalaRenderer.spec.ts | 17 +-- .../__snapshots__/ScalaGenerator.spec.ts.snap | 105 ++++++++++++++++++ .../DescriptionPreset.spec.ts.snap | 32 ++++++ 10 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 test/generators/scala/__snapshots__/ScalaGenerator.spec.ts.snap create mode 100644 test/generators/scala/presets/__snapshots__/DescriptionPreset.spec.ts.snap diff --git a/src/generators/scala/Constants.ts b/src/generators/scala/Constants.ts index 5ad81ff5e5..69415eeb33 100644 --- a/src/generators/scala/Constants.ts +++ b/src/generators/scala/Constants.ts @@ -39,16 +39,12 @@ export const RESERVED_SCALA_KEYWORDS = [ 'var', 'while', 'with', - 'yield', + 'yield' ]; export function isReservedScalaKeyword( word: string, forceLowerCase = true ): boolean { - return checkForReservedKeyword( - word, - RESERVED_SCALA_KEYWORDS, - forceLowerCase - ); + return checkForReservedKeyword(word, RESERVED_SCALA_KEYWORDS, forceLowerCase); } diff --git a/src/generators/scala/ScalaDependencyManager.ts b/src/generators/scala/ScalaDependencyManager.ts index d49d728a54..ce6e56e230 100644 --- a/src/generators/scala/ScalaDependencyManager.ts +++ b/src/generators/scala/ScalaDependencyManager.ts @@ -2,10 +2,7 @@ import { AbstractDependencyManager } from '../AbstractDependencyManager'; import { ScalaOptions } from './ScalaGenerator'; export class ScalaDependencyManager extends AbstractDependencyManager { - constructor( - public options: ScalaOptions, - dependencies: string[] = [] - ) { + constructor(public options: ScalaOptions, dependencies: string[] = []) { super(dependencies); } diff --git a/src/generators/scala/ScalaGenerator.ts b/src/generators/scala/ScalaGenerator.ts index 260f9b4dee..c765b8af8b 100644 --- a/src/generators/scala/ScalaGenerator.ts +++ b/src/generators/scala/ScalaGenerator.ts @@ -30,8 +30,7 @@ import { import { DeepPartial, mergePartialAndDefault } from '../../utils/Partials'; import { ScalaDependencyManager } from './ScalaDependencyManager'; -export interface ScalaOptions - extends CommonGeneratorOptions { +export interface ScalaOptions extends CommonGeneratorOptions { typeMapping: TypeMapping; constraints: Constraints; collectionType: 'List' | 'Array'; @@ -40,7 +39,7 @@ export interface ScalaOptions export type ScalaTypeMapping = TypeMapping< ScalaOptions, ScalaDependencyManager -> +>; export interface ScalaRenderCompleteModelOptions { packageName: string; @@ -66,9 +65,7 @@ export class ScalaGenerator extends AbstractGenerator< /** * Returns the Scala options by merging custom options with default ones. */ - static getScalaOptions( - options?: DeepPartial - ): ScalaOptions { + static getScalaOptions(options?: DeepPartial): ScalaOptions { const optionsToUse = mergePartialAndDefault( ScalaGenerator.defaultOptions, options @@ -86,9 +83,7 @@ export class ScalaGenerator extends AbstractGenerator< * Wrapper to get an instance of the dependency manager */ getDependencyManager(options: ScalaOptions): ScalaDependencyManager { - return this.getDependencyManagerInstance( - options - ) as ScalaDependencyManager; + return this.getDependencyManagerInstance(options) as ScalaDependencyManager; } /** diff --git a/src/generators/scala/renderers/ClassRenderer.ts b/src/generators/scala/renderers/ClassRenderer.ts index 90eb56f61f..4179174e52 100644 --- a/src/generators/scala/renderers/ClassRenderer.ts +++ b/src/generators/scala/renderers/ClassRenderer.ts @@ -7,8 +7,9 @@ import { ScalaOptions } from '../ScalaGenerator'; import { ClassPresetType } from '../ScalaPreset'; function getPropertyType(property: ConstrainedObjectPropertyModel): string { - const propertyType = property.required ? - property.property.type : `Option[${property.property.type}]`; + const propertyType = property.required + ? property.property.type + : `Option[${property.property.type}]`; return propertyType; } diff --git a/src/generators/scala/renderers/EnumRenderer.ts b/src/generators/scala/renderers/EnumRenderer.ts index 825bb68f73..310abaf173 100644 --- a/src/generators/scala/renderers/EnumRenderer.ts +++ b/src/generators/scala/renderers/EnumRenderer.ts @@ -5,7 +5,7 @@ import { } from '../../../models'; import { EnumPresetType } from '../ScalaPreset'; import { ScalaOptions } from '../ScalaGenerator'; -import { FormatHelpers } from 'helpers'; +import { FormatHelpers } from '../../../helpers'; /** * Renderer for Scala's `enum` type @@ -19,7 +19,7 @@ export class EnumRenderer extends ScalaRenderer { await this.runFromValuePreset(), await this.runAdditionalContentPreset() ]; - return `object ${this.model.name} extends Enumeration { + return `object ${this.model.name} extends Enumeration { type ${this.model.name} = Value ${this.indent(this.renderBlock(content, 2))} @@ -36,7 +36,7 @@ ${this.indent(this.renderBlock(content, 2))} } const content = items.join('\n'); - return `${content};`; + return `${content}`; } runItemPreset(item: ConstrainedEnumValueModel): Promise { @@ -52,9 +52,9 @@ export const SCALA_DEFAULT_ENUM_PRESET: EnumPresetType = { self({ renderer, model }) { return renderer.defaultSelf(model.type); }, - item({ item }) { + item({ item, model }) { const key = FormatHelpers.toPascalCase(item.key); - return `val ${key} = Value("${item.value}")` + return `val ${key}: ${model.name}.Value = Value(${item.value})`; } }; diff --git a/test/generators/scala/ScalaConstrainer.spec.ts b/test/generators/scala/ScalaConstrainer.spec.ts index b4c1e411b9..f1fd616ccf 100644 --- a/test/generators/scala/ScalaConstrainer.spec.ts +++ b/test/generators/scala/ScalaConstrainer.spec.ts @@ -54,7 +54,7 @@ describe('ScalaConstrainer', () => { options: ScalaGenerator.defaultOptions, dependencyManager: undefined as never }); - expect(type).toEqual(''); + expect(type).toEqual('Any'); }); }); describe('Float', () => { @@ -213,7 +213,7 @@ describe('ScalaConstrainer', () => { options: ScalaGenerator.defaultOptions, dependencyManager: undefined as never }); - expect(type).toEqual('ByteArray'); + expect(type).toEqual('Array[Byte]'); }); }); @@ -291,7 +291,7 @@ describe('ScalaConstrainer', () => { }; const type = ScalaDefaultTypeMapping.Array({ constrainedModel: model, - options: ScalaGenerator.defaultOptions, + options, dependencyManager: undefined as never }); expect(type).toEqual('Array[String]'); diff --git a/test/generators/scala/ScalaGenerator.spec.ts b/test/generators/scala/ScalaGenerator.spec.ts index 4b3731d042..bccf45b2a8 100644 --- a/test/generators/scala/ScalaGenerator.spec.ts +++ b/test/generators/scala/ScalaGenerator.spec.ts @@ -24,7 +24,7 @@ describe('ScalaGenerator', () => { expect(models[0].result).toMatchSnapshot(); }); - test('should render `data class` type', async () => { + test('should render `case class` type', async () => { const doc = { $id: 'Address', type: 'object', @@ -54,7 +54,7 @@ describe('ScalaGenerator', () => { type: 'string' } }, - required: ['street_name', 'city', 'state', 'house_number', 'array_type'] + required: ['street_name', 'city', 'state', 'house_number', 'array_type'], }; const expectedDependencies = []; @@ -166,7 +166,7 @@ describe('ScalaGenerator', () => { type: 'string' } }, - required: ['street_name', 'city', 'state', 'house_number', 'array_type'] + required: ['street_name', 'city', 'state', 'house_number', 'array_type'], }; const config = { packageName: 'test.package' }; const models = await generator.generateCompleteModels(doc, config); diff --git a/test/generators/scala/ScalaRenderer.spec.ts b/test/generators/scala/ScalaRenderer.spec.ts index 75b85b94d2..1b19e1b7e6 100644 --- a/test/generators/scala/ScalaRenderer.spec.ts +++ b/test/generators/scala/ScalaRenderer.spec.ts @@ -25,28 +25,17 @@ describe('ScalaRenderer', () => { describe('renderAnnotation()', () => { test('Should render', () => { - expect(renderer.renderAnnotation('someComment')).toEqual('@SomeComment'); + 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")'); + ).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' - ); + ).toEqual('@someComment(test="test2")'); }); }); }); diff --git a/test/generators/scala/__snapshots__/ScalaGenerator.spec.ts.snap b/test/generators/scala/__snapshots__/ScalaGenerator.spec.ts.snap new file mode 100644 index 0000000000..717eef2ed4 --- /dev/null +++ b/test/generators/scala/__snapshots__/ScalaGenerator.spec.ts.snap @@ -0,0 +1,105 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScalaGenerator should not render reserved keyword 1`] = ` +"case class Address( + reservedClass: Option[String], +)" +`; + +exports[`ScalaGenerator should render \`case class\` type 1`] = ` +"case class Address( + streetName: String, + city: String, + state: String, + houseNumber: Double, + marriage: Option[Boolean], + members: Option[Any], + arrayType: List[Any], + date: Option[java.time.LocalDate], + time: Option[java.time.OffsetTime], + dateTime: Option[java.time.OffsetDateTime], + binary: Option[Array[Byte]], + additionalProperties: Option[Map[String, Any]], +)" +`; + +exports[`ScalaGenerator should render \`enum class\` type (string type) 1`] = ` +"object States extends Enumeration { + type States = Value + + val Texas: States.Value = Value(\\"Texas\\") + val Alabama: States.Value = Value(\\"Alabama\\") + val California: States.Value = Value(\\"California\\") + val NewYork: States.Value = Value(\\"New York\\") +}" +`; + +exports[`ScalaGenerator should render \`enum\` type (integer type) 1`] = ` +"object Numbers extends Enumeration { + type Numbers = Value + + val Number_0: Numbers.Value = Value(0) + val Number_1: Numbers.Value = Value(1) + val Number_2: Numbers.Value = Value(2) + val Number_3: Numbers.Value = Value(3) +}" +`; + +exports[`ScalaGenerator should render \`enum\` type (union type) 1`] = ` +"object Union extends Enumeration { + type Union = Value + + val Texas: Union.Value = Value(\\"Texas\\") + val Alabama: Union.Value = Value(\\"Alabama\\") + val Number_0: Union.Value = Value(0) + val Number_1: Union.Value = Value(1) + val ReservedNumber_1: Union.Value = Value(\\"1\\") + val ReservedTrue: Union.Value = Value(\\"true\\") + val CurlyleftQuotationTestQuotationColonQuotationTestQuotationCurlyright: Union.Value = Value(\\"{\\\\\\"test\\\\\\":\\\\\\"test\\\\\\"}\\") +}" +`; + +exports[`ScalaGenerator should render List type for collections 1`] = ` +"case class CustomClass( + arrayType: Option[List[Int]], +)" +`; + +exports[`ScalaGenerator should render enums with translated special characters 1`] = ` +"object States extends Enumeration { + type States = Value + + val TestPlus: States.Value = Value(\\"test+\\") + val Test: States.Value = Value(\\"test\\") + val TestMinus: States.Value = Value(\\"test-\\") + val TestQuestionExclamation: States.Value = Value(\\"test?!\\") + val AsteriskTest: States.Value = Value(\\"*test\\") +}" +`; + +exports[`ScalaGenerator should render models and their dependencies 1`] = ` +"package test.\`package\` + + +case class Address( + streetName: String, + city: String, + state: String, + houseNumber: Double, + marriage: Option[Boolean], + members: Option[Any], + arrayType: List[Any], + otherModel: Option[OtherModel], + additionalProperties: Option[Map[String, Any]], +)" +`; + +exports[`ScalaGenerator should render models and their dependencies 2`] = ` +"package test.\`package\` + + +case class OtherModel( + streetName: Option[String], + additionalProperties: Option[Map[String, Any]], +)" +`; diff --git a/test/generators/scala/presets/__snapshots__/DescriptionPreset.spec.ts.snap b/test/generators/scala/presets/__snapshots__/DescriptionPreset.spec.ts.snap new file mode 100644 index 0000000000..9ed63ab635 --- /dev/null +++ b/test/generators/scala/presets/__snapshots__/DescriptionPreset.spec.ts.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SCALA_DESCRIPTION_PRESET should render description and examples for class 1`] = ` +"/** + * Description for class + * + * @property prop Description for prop + * @property additionalProperties + * + * Examples: + * {\\"prop\\":\\"value\\"} + */ +case class Clazz( + prop: Option[String], + additionalProperties: Option[Map[String, Any]], +)" +`; + +exports[`SCALA_DESCRIPTION_PRESET should render description and examples for enum 1`] = ` +"/** + * Description for enum + * + * Examples: + * value + */ +object Enum extends Enumeration { + type Enum = Value + + val On: Enum.Value = Value(\\"on\\") + val Off: Enum.Value = Value(\\"off\\") +}" +`; From be01beb1a2c0bc68afccb0c2ab6de5c61fe0bc27 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Sun, 17 Dec 2023 11:37:31 +0200 Subject: [PATCH 6/8] Fix style and Sonar complaints --- package.json | 3 ++- src/generators/scala/ScalaDependencyManager.ts | 5 ++++- src/generators/scala/renderers/ClassRenderer.ts | 4 +--- src/generators/scala/renderers/EnumRenderer.ts | 4 ++-- test/generators/scala/ScalaGenerator.spec.ts | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index bc7a340031..375294a7f8 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,8 @@ "generate:assets": "npm run build:prod && npm run docs && npm run generate:readme:toc && npm run format", "bump:version": "npm --no-git-tag-version --allow-same-version version $VERSION", "prepublishOnly": "npm run build:prod && npm run generate:readme:toc && npm run format", - "format": "prettier --config .prettierrc \"./**/*.ts\" --write" + "format": "prettier --config .prettierrc \"./**/*.ts\" --write", + "format-now": "prettier --config .prettierrc test/generators/scala/ScalaGenerator.spec.ts --write" }, "publishConfig": { "access": "public" diff --git a/src/generators/scala/ScalaDependencyManager.ts b/src/generators/scala/ScalaDependencyManager.ts index ce6e56e230..d49d728a54 100644 --- a/src/generators/scala/ScalaDependencyManager.ts +++ b/src/generators/scala/ScalaDependencyManager.ts @@ -2,7 +2,10 @@ import { AbstractDependencyManager } from '../AbstractDependencyManager'; import { ScalaOptions } from './ScalaGenerator'; export class ScalaDependencyManager extends AbstractDependencyManager { - constructor(public options: ScalaOptions, dependencies: string[] = []) { + constructor( + public options: ScalaOptions, + dependencies: string[] = [] + ) { super(dependencies); } diff --git a/src/generators/scala/renderers/ClassRenderer.ts b/src/generators/scala/renderers/ClassRenderer.ts index 4179174e52..d175300b07 100644 --- a/src/generators/scala/renderers/ClassRenderer.ts +++ b/src/generators/scala/renderers/ClassRenderer.ts @@ -7,11 +7,9 @@ import { ScalaOptions } from '../ScalaGenerator'; import { ClassPresetType } from '../ScalaPreset'; function getPropertyType(property: ConstrainedObjectPropertyModel): string { - const propertyType = property.required + return property.required ? property.property.type : `Option[${property.property.type}]`; - - return propertyType; } /** diff --git a/src/generators/scala/renderers/EnumRenderer.ts b/src/generators/scala/renderers/EnumRenderer.ts index 310abaf173..b793355f0d 100644 --- a/src/generators/scala/renderers/EnumRenderer.ts +++ b/src/generators/scala/renderers/EnumRenderer.ts @@ -13,7 +13,7 @@ import { FormatHelpers } from '../../../helpers'; * @extends ScalaRenderer */ export class EnumRenderer extends ScalaRenderer { - async defaultSelf(valueType: string): Promise { + async defaultSelf(): Promise { const content = [ await this.renderItems(), await this.runFromValuePreset(), @@ -50,7 +50,7 @@ ${this.indent(this.renderBlock(content, 2))} export const SCALA_DEFAULT_ENUM_PRESET: EnumPresetType = { self({ renderer, model }) { - return renderer.defaultSelf(model.type); + return renderer.defaultSelf(); }, item({ item, model }) { const key = FormatHelpers.toPascalCase(item.key); diff --git a/test/generators/scala/ScalaGenerator.spec.ts b/test/generators/scala/ScalaGenerator.spec.ts index bccf45b2a8..7fab2302cd 100644 --- a/test/generators/scala/ScalaGenerator.spec.ts +++ b/test/generators/scala/ScalaGenerator.spec.ts @@ -54,7 +54,7 @@ describe('ScalaGenerator', () => { type: 'string' } }, - required: ['street_name', 'city', 'state', 'house_number', 'array_type'], + required: ['street_name', 'city', 'state', 'house_number', 'array_type'] }; const expectedDependencies = []; @@ -166,7 +166,7 @@ describe('ScalaGenerator', () => { type: 'string' } }, - required: ['street_name', 'city', 'state', 'house_number', 'array_type'], + required: ['street_name', 'city', 'state', 'house_number', 'array_type'] }; const config = { packageName: 'test.package' }; const models = await generator.generateCompleteModels(doc, config); From 9b91c3fb3257a9975665ffb64c58d891210c1404 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Sun, 17 Dec 2023 11:44:48 +0200 Subject: [PATCH 7/8] Make Sonar happy --- src/generators/scala/renderers/EnumRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/scala/renderers/EnumRenderer.ts b/src/generators/scala/renderers/EnumRenderer.ts index b793355f0d..45986fb6a8 100644 --- a/src/generators/scala/renderers/EnumRenderer.ts +++ b/src/generators/scala/renderers/EnumRenderer.ts @@ -49,7 +49,7 @@ ${this.indent(this.renderBlock(content, 2))} } export const SCALA_DEFAULT_ENUM_PRESET: EnumPresetType = { - self({ renderer, model }) { + self({ renderer }) { return renderer.defaultSelf(); }, item({ item, model }) { From ad94e426146fe8226136a5bc89281005d1ee6e82 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Sat, 23 Dec 2023 15:06:49 +0200 Subject: [PATCH 8/8] Adding Scala code gen examples --- examples/generate-scala-enums/README.md | 17 ++++++++ examples/generate-scala-enums/index.spec.ts | 15 +++++++ examples/generate-scala-enums/index.ts | 20 +++++++++ .../generate-scala-enums/package-lock.json | 10 +++++ examples/generate-scala-enums/package.json | 12 ++++++ examples/generate-scala-models/README.md | 17 ++++++++ examples/generate-scala-models/index.spec.ts | 15 +++++++ examples/generate-scala-models/index.ts | 41 +++++++++++++++++++ .../generate-scala-models/package-lock.json | 10 +++++ examples/generate-scala-models/package.json | 12 ++++++ package.json | 3 +- 11 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 examples/generate-scala-enums/README.md create mode 100644 examples/generate-scala-enums/index.spec.ts create mode 100644 examples/generate-scala-enums/index.ts create mode 100644 examples/generate-scala-enums/package-lock.json create mode 100644 examples/generate-scala-enums/package.json create mode 100644 examples/generate-scala-models/README.md create mode 100644 examples/generate-scala-models/index.spec.ts create mode 100644 examples/generate-scala-models/index.ts create mode 100644 examples/generate-scala-models/package-lock.json create mode 100644 examples/generate-scala-models/package.json diff --git a/examples/generate-scala-enums/README.md b/examples/generate-scala-enums/README.md new file mode 100644 index 0000000000..167791ec4a --- /dev/null +++ b/examples/generate-scala-enums/README.md @@ -0,0 +1,17 @@ +# Scala Enums + +A basic example of how to use Modelina and output a Scala enumeration. + +## 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-scala-enums/index.spec.ts b/examples/generate-scala-enums/index.spec.ts new file mode 100644 index 0000000000..7afa11cb9c --- /dev/null +++ b/examples/generate-scala-enums/index.spec.ts @@ -0,0 +1,15 @@ +const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { + return; +}); +import { generate } from './index'; + +describe('Should be able to render Kotlin Enums', () => { + 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-scala-enums/index.ts b/examples/generate-scala-enums/index.ts new file mode 100644 index 0000000000..458cc94c8d --- /dev/null +++ b/examples/generate-scala-enums/index.ts @@ -0,0 +1,20 @@ +import { ScalaGenerator } from '../../src/generators/scala'; + +const generator = new ScalaGenerator(); +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-scala-enums/package-lock.json b/examples/generate-scala-enums/package-lock.json new file mode 100644 index 0000000000..71205d5b99 --- /dev/null +++ b/examples/generate-scala-enums/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "generate-scala-enums", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/generate-scala-enums/package.json b/examples/generate-scala-enums/package.json new file mode 100644 index 0000000000..f3f7301a7c --- /dev/null +++ b/examples/generate-scala-enums/package.json @@ -0,0 +1,12 @@ +{ + "config": { + "example_name": "generate-scala-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/examples/generate-scala-models/README.md b/examples/generate-scala-models/README.md new file mode 100644 index 0000000000..c8770e41e7 --- /dev/null +++ b/examples/generate-scala-models/README.md @@ -0,0 +1,17 @@ +# Scala Data Models + +A basic example of how to use Modelina and output a Scala 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-scala-models/index.spec.ts b/examples/generate-scala-models/index.spec.ts new file mode 100644 index 0000000000..010afc6245 --- /dev/null +++ b/examples/generate-scala-models/index.spec.ts @@ -0,0 +1,15 @@ +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(3); + expect(spy.mock.calls[0]).toMatchSnapshot(); + }); +}); diff --git a/examples/generate-scala-models/index.ts b/examples/generate-scala-models/index.ts new file mode 100644 index 0000000000..74cde5421c --- /dev/null +++ b/examples/generate-scala-models/index.ts @@ -0,0 +1,41 @@ +import { ScalaGenerator } from '../../src/generators/scala'; + +const generator = new ScalaGenerator(); +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-scala-models/package-lock.json b/examples/generate-scala-models/package-lock.json new file mode 100644 index 0000000000..61b5cb38ef --- /dev/null +++ b/examples/generate-scala-models/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "generate-scala-models", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/generate-scala-models/package.json b/examples/generate-scala-models/package.json new file mode 100644 index 0000000000..5ca775bc64 --- /dev/null +++ b/examples/generate-scala-models/package.json @@ -0,0 +1,12 @@ +{ + "config": { + "example_name": "generate-scala-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/package.json b/package.json index 375294a7f8..bc7a340031 100644 --- a/package.json +++ b/package.json @@ -127,8 +127,7 @@ "generate:assets": "npm run build:prod && npm run docs && npm run generate:readme:toc && npm run format", "bump:version": "npm --no-git-tag-version --allow-same-version version $VERSION", "prepublishOnly": "npm run build:prod && npm run generate:readme:toc && npm run format", - "format": "prettier --config .prettierrc \"./**/*.ts\" --write", - "format-now": "prettier --config .prettierrc test/generators/scala/ScalaGenerator.spec.ts --write" + "format": "prettier --config .prettierrc \"./**/*.ts\" --write" }, "publishConfig": { "access": "public"