Skip to content

Commit

Permalink
Adding Scala specific generator code
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur Ciocanu committed Dec 16, 2023
1 parent 80c3e34 commit 3864fa2
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 148 deletions.
42 changes: 41 additions & 1 deletion src/generators/scala/Constants.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
144 changes: 119 additions & 25 deletions src/generators/scala/ScalaConstrainer.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,149 @@
import { Constraints, TypeMapping } from '../../helpers';
import { Constraints } from '../../helpers';
import { ConstrainedEnumValueModel } from '../../models';
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';
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}]`;
}
};

Expand Down
9 changes: 9 additions & 0 deletions src/generators/scala/ScalaDependencyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
}
10 changes: 4 additions & 6 deletions src/generators/scala/ScalaFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,23 @@ 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<string, unknown> | InputMetaModel,
outputDirectory: string,
options?: ScalaRenderCompleteModelOptions,
options: ScalaRenderCompleteModelOptions,
ensureFilesWritten = false
): Promise<OutputModel[]> {
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 !== '';
});
for (const outputModel of generatedModels) {
const filePath = path.resolve(
outputDirectory,
`${outputModel.modelName}.MYEXTENSION`
`${outputModel.modelName}.scala`
);
await FileHelpers.writerToFileSystem(
outputModel.result,
Expand Down
74 changes: 47 additions & 27 deletions src/generators/scala/ScalaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,30 @@ export interface ScalaOptions
extends CommonGeneratorOptions<ScalaPreset> {
typeMapping: TypeMapping<ScalaOptions, ScalaDependencyManager>;
constraints: Constraints;
collectionType: 'List' | 'Array';
}

export type ScalaTypeMapping = TypeMapping<
ScalaOptions,
ScalaDependencyManager
>

export interface ScalaRenderCompleteModelOptions {
packageName: string;
}

export class ScalaGenerator extends AbstractGenerator<
ScalaOptions,
ScalaRenderCompleteModelOptions
> {
static defaultOptions: ScalaOptions = {
...defaultGeneratorOptions,
defaultPreset: SCALA_DEFAULT_PRESET,
collectionType: 'List',
typeMapping: ScalaDefaultTypeMapping,
constraints: ScalaDefaultConstraints
};

static defaultCompleteModelOptions: ScalaRenderCompleteModelOptions = {
packageName: 'Asyncapi.Models'
};

constructor(options?: DeepPartial<ScalaOptions>) {
const realizedOptions = ScalaGenerator.getScalaOptions(options);
super('Scala', realizedOptions);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -128,10 +132,22 @@ export class ScalaGenerator extends AbstractGenerator<
render(
args: AbstractGeneratorRenderArgs<ScalaOptions>
): Promise<RenderOutput> {
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}`
Expand Down Expand Up @@ -160,27 +176,20 @@ export class ScalaGenerator extends AbstractGenerator<
ScalaRenderCompleteModelOptions
>
): Promise<RenderOutput> {
const completeModelOptionsToUse =
mergePartialAndDefault<ScalaRenderCompleteModelOptions>(
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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 3864fa2

Please sign in to comment.