Skip to content

Commit

Permalink
feat: added C# class and enum generator (#316)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaslagoni authored Aug 12, 2021
1 parent e57110b commit 1dfbe58
Show file tree
Hide file tree
Showing 21 changed files with 973 additions and 15 deletions.
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

0 comments on commit 1dfbe58

Please sign in to comment.