Skip to content

Commit

Permalink
feat: adding Scala generator (#1665)
Browse files Browse the repository at this point in the history
Co-authored-by: Artur Ciocanu <[email protected]>
  • Loading branch information
artur-ciocanu and Artur Ciocanu authored Dec 23, 2023
1 parent 1237a8c commit 26a3953
Show file tree
Hide file tree
Showing 34 changed files with 2,206 additions and 0 deletions.
17 changes: 17 additions & 0 deletions examples/generate-scala-enums/README.md
Original file line number Diff line number Diff line change
@@ -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
```
15 changes: 15 additions & 0 deletions examples/generate-scala-enums/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
20 changes: 20 additions & 0 deletions examples/generate-scala-enums/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const models = await generator.generate(jsonSchemaDraft7);
for (const model of models) {
console.log(model.result);
}
}
if (require.main === module) {
generate();
}
10 changes: 10 additions & 0 deletions examples/generate-scala-enums/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions examples/generate-scala-enums/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
17 changes: 17 additions & 0 deletions examples/generate-scala-models/README.md
Original file line number Diff line number Diff line change
@@ -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
```
15 changes: 15 additions & 0 deletions examples/generate-scala-models/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
41 changes: 41 additions & 0 deletions examples/generate-scala-models/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const models = await generator.generate(jsonSchemaDraft7);
for (const model of models) {
console.log(model.result);
}
}
if (require.main === module) {
generate();
}
10 changes: 10 additions & 0 deletions examples/generate-scala-models/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions examples/generate-scala-models/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
50 changes: 50 additions & 0 deletions src/generators/scala/Constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { checkForReservedKeyword } from '../../helpers';

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,
forceLowerCase = true
): boolean {
return checkForReservedKeyword(word, RESERVED_SCALA_KEYWORDS, forceLowerCase);
}
156 changes: 156 additions & 0 deletions src/generators/scala/ScalaConstrainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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 { ScalaTypeMapping } from './ScalaGenerator';

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 {
return constrainedModel.name;
},
Reference({ constrainedModel }): string {
return constrainedModel.name;
},
Any(): string {
return 'Any';
},
Float({ constrainedModel }): string {
return constrainedModel.options.format === 'float' ? 'Float' : 'Double';
},
Integer({ constrainedModel }): string {
return constrainedModel.options.format === 'long' ||
constrainedModel.options.format === 'int64'
? 'Long'
: 'Int';
},
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 'Boolean';
},
// Since there are not tuples in Kotlin, we have to return a collection of `Any`
Tuple({ options }): string {
const isList = options.collectionType && options.collectionType === 'List';

return isList ? 'List[Any]' : 'Array[Any]';
},
Array({ constrainedModel, options }): string {
const isList = options.collectionType && options.collectionType === 'List';
const type = constrainedModel.valueModel.type;

return isList ? `List[${type}]` : `Array[${type}]`;
},
Enum({ constrainedModel }): string {
const 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 {
// No Unions in Kotlin, use Any for now.
return 'Any';
},
Dictionary({ constrainedModel }): string {
return `Map[${constrainedModel.key.type}, ${constrainedModel.value.type}]`;
}
};

export const ScalaDefaultConstraints: Constraints = {
enumKey: defaultEnumKeyConstraints(),
enumValue: defaultEnumValueConstraints(),
modelName: defaultModelNameConstraints(),
propertyKey: defaultPropertyKeyConstraints(),
constant: defaultConstantConstraints()
};
Loading

0 comments on commit 26a3953

Please sign in to comment.