Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added C# class and enum generator #316

Merged
merged 14 commits into from
Aug 12, 2021
Merged
10 changes: 9 additions & 1 deletion .github/workflows/blackbox-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
jobs:
test:
if: github.event.pull_request.draft == false
name: TypeScript ${{ matrix.os }}
name: BlackBox testing ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
Expand All @@ -24,5 +24,13 @@ jobs:
with:
distribution: 'adopt'
java-version: '11'
- if: matrix.os != 'windows-latest'
name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: '5.0.x'
- if: matrix.os == 'windows-latest'
name: Setup csc.exe
uses: yoavain/Setup-CSC@v7
- name: Test output
run: npm run test:blackbox
22 changes: 18 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
FROM openjdk:16.0.1-jdk-slim-buster

# Install updates
RUN apt-get update -yq \
&& apt-get install -yq curl \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -yq nodejs \
&& curl -fsSL https://golang.org/dl/go1.16.5.linux-amd64.tar.gz | tar -C /usr/local -xz
&& apt-get install -yq curl

# Install nodejs
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -yq nodejs

# Install golang
RUN curl -fsSL https://golang.org/dl/go1.16.5.linux-amd64.tar.gz | tar -C /usr/local -xz
ENV PATH="${PATH}:/usr/local/go/bin"

# Install dotnet SDK
RUN apt install apt-transport-https dirmngr gnupg ca-certificates -yq \
&& apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF \
&& echo "deb https://download.mono-project.com/repo/debian stable-buster main" | tee /etc/apt/sources.list.d/mono-official-stable.list \
&& apt update -yq \
&& apt install mono-devel -yq

# Setup library
COPY package-lock.json .
RUN npm install
COPY . .
1 change: 1 addition & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ If you prefer to install all the dependencies locally, keep reading.
The blackbox testing have some different requirements in order to successfully run:
- To to run the `java` blackbox tests, you need to have JDK installed.
- To to run the `ts` blackbox tests, you need to have TypeScript installed globally - `npm install -g typescript`.
- To to run the `C#` blackbox tests, you need to have C# compiler installed globally. - https://www.mono-project.com/download/stable/

By default the blackbox tests are not run with the regular `npm run test`, but can be run with `npm run test:blackbox`.
9 changes: 9 additions & 0 deletions docs/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [TypeScript](../src/generators/typescript/TypeScriptGenerator.ts),
- [Java](../src/generators/java/JavaGenerator.ts).
- [Go](../src/generators/go/GoGenerator.ts).
- [C#](../src/generators/csharp/CSharpGenerator.ts).

## Generator's options

Expand Down Expand Up @@ -66,6 +67,14 @@ Below is a list of additional options available for a given generator.
| `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name_ |
| `namingConvention.field` | Function | A function that returns the format of the field. | _Returns pascal cased name_ |

### [C#](../src/generators/csharp/CSharpGenerator.ts)

| Option | Type | Description | Default value |
|---|---|---|---|$
| `namingConvention` | Object | Options for naming conventions. | - |
| `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name, and ensures that reserved keywords are never rendered__ |
| `namingConvention.property` | Function | A function that returns the format of the property. | _Returns camel cased name, and ensures that names of properties does not clash against reserved keywords_ |

## Custom generator

The minimum set of required actions to create a new generator are:
Expand Down
18 changes: 18 additions & 0 deletions docs/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,21 @@ There are no additional methods.
| Method | Description | Additional arguments |
|---|---|---|
| `field` | A method to extend rendered given field. | `fieldName` as a name of a given field, `field` object as a [`CommonModel`](../src/models/CommonModel.ts) instance. |


### C#

#### **Class**

| Method | Description | Additional arguments |
|---|---|---|
| `ctor` | A method to extend rendered constructor for a given class. | - |
| `property` | A method to extend rendered given property. | `propertyName` as a name of a given property, `property` object as a [`CommonModel`](../src/models/CommonModel.ts) instance. |
| `setter` | A method to extend setter for a given property. | `propertyName` as a name of a given property, `property` object as a [`CommonModel`](../src/models/CommonModel.ts) instance. |
| `getter` | A method to extend getter for a given property. | `propertyName` as a name of a given property, `property` object as a [`CommonModel`](../src/models/CommonModel.ts) instance. |

#### **Enum**

| Method | Description | Additional arguments |
|---|---|---|
| `item` | A method to extend enum's item. | an `item` containing the value of enum's item. |
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"build:cjs": "tsc",
"build:esm": "tsc --project tsconfig.json --module ESNext --outDir ./lib/esm",
"build:types": "tsc --project tsconfig.json --declaration --emitDeclarationOnly --declarationMap --outDir ./lib/types",
"docker:build": "docker build -q -t asyncapi/modelina .",
"docker:build": "docker build -t asyncapi/modelina .",
"docker:test": "npm run docker:build && docker run asyncapi/modelina npm run test",
"docker:test:blackbox": "npm run docker:build && docker run asyncapi/modelina npm run test:blackbox",
"test": "cross-env CI=true jest --coverage --testPathIgnorePatterns ./test/blackbox",
Expand Down
57 changes: 57 additions & 0 deletions src/generators/csharp/CSharpGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
AbstractGenerator,
CommonGeneratorOptions,
defaultGeneratorOptions,
} from '../AbstractGenerator';
import { CommonModel, CommonInputModel, RenderOutput } from '../../models';
import { TypeHelpers, ModelKind, CommonNamingConvention, CommonNamingConventionImplementation } from '../../helpers';
import { CSharpPreset, CSHARP_DEFAULT_PRESET } from './CSharpPreset';
import { EnumRenderer } from './renderers/EnumRenderer';
import { ClassRenderer } from './renderers/ClassRenderer';

export interface CSharpOptions extends CommonGeneratorOptions<CSharpPreset> {
namingConvention?: CommonNamingConvention;
}

/**
* Generator for CSharp
*/
export class CSharpGenerator extends AbstractGenerator<CSharpOptions> {
static defaultOptions: CSharpOptions = {
...defaultGeneratorOptions,
defaultPreset: CSHARP_DEFAULT_PRESET,
namingConvention: CommonNamingConventionImplementation
};

constructor(
options: CSharpOptions = CSharpGenerator.defaultOptions,
) {
super('CSharp', CSharpGenerator.defaultOptions, options);
}

render(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const kind = TypeHelpers.extractKind(model);
switch (kind) {
case ModelKind.OBJECT:
return this.renderClass(model, inputModel);
case ModelKind.ENUM:
return this.renderEnum(model, inputModel);
}

return Promise.resolve(RenderOutput.toRenderOutput({ result: '', dependencies: [] }));
}

async renderEnum(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const presets = this.getPresets('enum');
const renderer = new EnumRenderer(this.options, this, presets, model, inputModel);
const result = await renderer.runSelfPreset();
return RenderOutput.toRenderOutput({ result, dependencies: renderer.dependencies });
}

async renderClass(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const presets = this.getPresets('class');
const renderer = new ClassRenderer(this.options, this, presets, model, inputModel);
const result = await renderer.runSelfPreset();
return RenderOutput.toRenderOutput({ result, dependencies: renderer.dependencies });
}
}
14 changes: 14 additions & 0 deletions src/generators/csharp/CSharpPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/ban-types */
import { Preset, EnumPreset, ClassPreset } from '../../models';
import { ClassRenderer, CSHARP_DEFAULT_CLASS_PRESET } from './renderers/ClassRenderer';
import { CSHARP_DEFAULT_ENUM_PRESET, EnumRenderer } from './renderers/EnumRenderer';

export type CSharpPreset = Preset<{
class: ClassPreset<ClassRenderer>;
enum: EnumPreset<EnumRenderer>
}>;

export const CSHARP_DEFAULT_PRESET: CSharpPreset = {
class: CSHARP_DEFAULT_CLASS_PRESET,
enum: CSHARP_DEFAULT_ENUM_PRESET,
};
96 changes: 96 additions & 0 deletions src/generators/csharp/CSharpRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { AbstractRenderer } from '../AbstractRenderer';
import { CSharpGenerator, CSharpOptions } from './CSharpGenerator';
import { CommonModel, CommonInputModel, Preset, PropertyType } from '../../models';
import { FormatHelpers } from '../../helpers/FormatHelpers';
import { isReservedCSharpKeyword } from './Constants';

/**
* Common renderer for CSharp types
*
* @extends AbstractRenderer
*/
export abstract class CSharpRenderer extends AbstractRenderer<CSharpOptions> {
constructor(
options: CSharpOptions,
generator: CSharpGenerator,
presets: Array<[Preset, unknown]>,
model: CommonModel,
inputModel: CommonInputModel,
) {
super(options, generator, presets, model, inputModel);
}

/**
* Renders the name of a type based on provided generator option naming convention type function.
*
* This is used to render names of models and then later used if that class is referenced from other models.
*
* @param name
* @param model
*/
nameType(name: string | undefined, model?: CommonModel): string {
return this.options?.namingConvention?.type
? this.options.namingConvention.type(name, { model: model || this.model, inputModel: this.inputModel, isReservedKeyword: isReservedCSharpKeyword(`${name}`) })
: name || '';
}

/**
* Renders the name of a property based on provided generator option naming convention property function.
*
* @param propertyName
* @param property
*/
nameProperty(propertyName: string | undefined, property?: CommonModel): string {
return this.options?.namingConvention?.property
? this.options.namingConvention.property(propertyName, { model: this.model, inputModel: this.inputModel, property, isReservedKeyword: isReservedCSharpKeyword(`${propertyName}`) })
: propertyName || '';
}

runPropertyPreset(propertyName: string, property: CommonModel, type: PropertyType = PropertyType.property): Promise<string> {
return this.runPreset('property', { propertyName, property, type });
}

renderType(model: CommonModel): string {
if (model.$ref !== undefined) {
return this.nameType(model.$ref);
}

if (Array.isArray(model.type)) {
return model.type.length > 1 ? 'dynamic' : `${this.toCSharpType(model.type[0], model)}`;
}

return this.toCSharpType(model.type, model);
}

renderComments(lines: string | string[]): string {
lines = FormatHelpers.breakLines(lines);
return lines.map(line => `// ${line}`).join('\n');
}

toCSharpType(type: string | undefined, model: CommonModel): string {
if (type === undefined) {
return 'dynamic';
}

switch (type) {
case 'string':
return 'string';
case 'integer':
return 'int';
case 'number':
return 'float';
case 'boolean':
return 'bool';
case 'object':
return 'object';
case 'array': {
if (Array.isArray(model.items)) {
return model.items.length > 1? 'dynamic[]' : `${this.renderType(model.items[0])}[]`;
}
const arrayType = model.items ? this.renderType(model.items) : 'dynamic';
return `${arrayType}[]`;
}
default: return type;
}
}
}
83 changes: 83 additions & 0 deletions src/generators/csharp/Constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
export const RESERVED_CSHARP_KEYWORDS = [
'abstract',
'as',
'base',
'bool',
'break',
'byte',
'case',
'catch',
'char',
'checked',
'class',
'const',
'continue',
'decimal',
'default',
'delegate',
'do',
'double',
'else',
'enum',
'event',
'explicit',
'extern',
'false',
'finally',
'fixed',
'float',
'for',
'foreach',
'goto',
'if',
'implicit',
'in',
'int',
'interface',
'internal',
'is',
'lock',
'long',
'namespace',
'new',
'null',
'object',
'operator',
'out',
'override',
'params',
'private',
'protected',
'public',
'readonly',
'ref',
'return',
'sbyte',
'sealed',
'short',
'sizeof',
'stackalloc',
'static',
'string',
'struct',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'uint',
'ulong',
'unchecked',
'unsafe',
'ushort',
'using',
'virtual',
'void',
'volatile',
'while'
];

export function isReservedCSharpKeyword(word: string): boolean {
return RESERVED_CSHARP_KEYWORDS.includes(word);
}
3 changes: 3 additions & 0 deletions src/generators/csharp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './CSharpGenerator';
export { CSHARP_DEFAULT_PRESET } from './CSharpPreset';
export type { CSharpPreset } from './CSharpPreset';
Loading