diff --git a/docs/contributing.md b/docs/contributing.md index 69a86ded1b..5e50e76c29 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -65,7 +65,7 @@ describe('LANGUAGE_MY_PRESET', () => { beforeEach(() => { generator = new LanguageGenerator({ presets: [LANGUAGE_MY_PRESET] }); }); - + test('should render xxx', async () => { const input = { $id: 'Clazz', diff --git a/docs/presets.md b/docs/presets.md index d32d6ca108..8a4a0a59ae 100644 --- a/docs/presets.md +++ b/docs/presets.md @@ -65,7 +65,7 @@ class Root { } ``` -The preset renderes the TypeScript class by calling **preset hooks**, which is callbacks that is called for rendering parts of the class. +The generator renderes the TypeScript class by calling **preset hooks**, which is callbacks that is called for rendering parts of the class. ```html diff --git a/package-lock.json b/package-lock.json index 1bd50916fe..c7730e5d9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18522,6 +18522,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", "dev": true, "dependencies": { "browser-process-hrtime": "^1.0.0" diff --git a/src/generators/csharp/CSharpFileGenerator.ts b/src/generators/csharp/CSharpFileGenerator.ts index 52eb9162b7..028bada26c 100644 --- a/src/generators/csharp/CSharpFileGenerator.ts +++ b/src/generators/csharp/CSharpFileGenerator.ts @@ -11,14 +11,15 @@ export class CSharpFileGenerator extends CSharpGenerator implements AbstractFile * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: CSharpRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: CSharpRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); for (const outputModel of generatedModels) { const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.cs`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } diff --git a/src/generators/dart/DartFileGenerator.ts b/src/generators/dart/DartFileGenerator.ts index fc00ce26b4..1e230f69f6 100644 --- a/src/generators/dart/DartFileGenerator.ts +++ b/src/generators/dart/DartFileGenerator.ts @@ -11,14 +11,15 @@ export class DartFileGenerator extends DartGenerator implements AbstractFileGene * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: DartRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: DartRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); for (const outputModel of generatedModels) { const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.dart`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } diff --git a/src/generators/go/GoFileGenerator.ts b/src/generators/go/GoFileGenerator.ts index 5e050396f9..8bb11d3bb5 100644 --- a/src/generators/go/GoFileGenerator.ts +++ b/src/generators/go/GoFileGenerator.ts @@ -11,14 +11,15 @@ export class GoFileGenerator extends GoGenerator implements AbstractFileGenerato * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: GoRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: GoRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); for (const outputModel of generatedModels) { const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.go`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } diff --git a/src/generators/java/JavaConstrainer.ts b/src/generators/java/JavaConstrainer.ts index 3e7005de63..2cbb94dfd7 100644 --- a/src/generators/java/JavaConstrainer.ts +++ b/src/generators/java/JavaConstrainer.ts @@ -1,4 +1,4 @@ -import { ConstrainedEnumValueModel } from 'models'; +import { ConstrainedEnumValueModel } from '../../models'; import { TypeMapping } from '../../helpers'; import { defaultEnumKeyConstraints, defaultEnumValueConstraints } from './constrainer/EnumConstrainer'; import { defaultModelNameConstraints } from './constrainer/ModelNameConstrainer'; diff --git a/src/generators/java/JavaFileGenerator.ts b/src/generators/java/JavaFileGenerator.ts index 8c6f073a8b..50c0405ccd 100644 --- a/src/generators/java/JavaFileGenerator.ts +++ b/src/generators/java/JavaFileGenerator.ts @@ -11,14 +11,15 @@ export class JavaFileGenerator extends JavaGenerator implements AbstractFileGene * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: JavaRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: JavaRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); for (const outputModel of generatedModels) { const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.java`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } diff --git a/src/generators/java/JavaPreset.ts b/src/generators/java/JavaPreset.ts index 51cd8efe6a..b33b3f7d31 100644 --- a/src/generators/java/JavaPreset.ts +++ b/src/generators/java/JavaPreset.ts @@ -1,5 +1,4 @@ -import { ConstrainedEnumModel } from 'models/ConstrainedMetaModel'; -import { Preset, ClassPreset, EnumPreset, PresetArgs, EnumArgs } from '../../models'; +import { Preset, ClassPreset, EnumPreset, PresetArgs, EnumArgs, ConstrainedEnumModel } from '../../models'; import { JavaOptions } from './JavaGenerator'; import { ClassRenderer, JAVA_DEFAULT_CLASS_PRESET } from './renderers/ClassRenderer'; import { EnumRenderer, JAVA_DEFAULT_ENUM_PRESET } from './renderers/EnumRenderer'; diff --git a/src/generators/javascript/JavaScriptFileGenerator.ts b/src/generators/javascript/JavaScriptFileGenerator.ts index 6a262f033f..21fab979cd 100644 --- a/src/generators/javascript/JavaScriptFileGenerator.ts +++ b/src/generators/javascript/JavaScriptFileGenerator.ts @@ -11,14 +11,15 @@ export class JavaScriptFileGenerator extends JavaScriptGenerator implements Abst * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options?: JavaScriptRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options?: JavaScriptRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options || {}); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); for (const outputModel of generatedModels) { const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.js`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } diff --git a/src/generators/python/PythonFileGenerator.ts b/src/generators/python/PythonFileGenerator.ts index ffd64f3527..0b86923c74 100644 --- a/src/generators/python/PythonFileGenerator.ts +++ b/src/generators/python/PythonFileGenerator.ts @@ -11,14 +11,15 @@ export class PythonFileGenerator extends PythonGenerator implements AbstractFile * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: PythonRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: PythonRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); for (const outputModel of generatedModels) { const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.py`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } diff --git a/src/generators/rust/RustFileGenerator.ts b/src/generators/rust/RustFileGenerator.ts index ed8f294f80..e3a2a9cb03 100644 --- a/src/generators/rust/RustFileGenerator.ts +++ b/src/generators/rust/RustFileGenerator.ts @@ -11,15 +11,16 @@ export class RustFileGenerator extends RustGenerator implements AbstractFileGene * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: RustRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options: RustRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== '' && outputModel.modelName !== undefined; }); for (const outputModel of generatedModels) { const fileName = FormatHelpers.snakeCase(outputModel.modelName); const filePath = path.resolve(outputDirectory, `${fileName}.rs`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } @@ -30,18 +31,19 @@ export class RustFileGenerator extends RustGenerator implements AbstractFileGene * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToPackage(input: Record | InputMetaModel, outputDirectory: string, options: RustRenderCompleteModelOptions): Promise { + public async generateToPackage(input: Record | InputMetaModel, outputDirectory: string, options: RustRenderCompleteModelOptions, ensureFilesWritten = false): Promise { // Crate package expects source code to be written to src/.rs const sourceOutputDirectory = `${outputDirectory}/src`; - let generatedModels = await this.generateToFiles(input, sourceOutputDirectory, options); + let generatedModels = await this.generateToFiles(input, sourceOutputDirectory, options, ensureFilesWritten); // render lib.rs and Cargo.toml if (options.supportFiles) { const supportOutput = await this.generateCompleteSupport(input, options); generatedModels = generatedModels.concat(supportOutput); for (const outputModel of supportOutput) { const filePath = path.resolve(outputDirectory, outputModel.model.name); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } } return generatedModels; diff --git a/src/generators/rust/constrainer/EnumConstrainer.ts b/src/generators/rust/constrainer/EnumConstrainer.ts index 3a1c49d255..44b86ba045 100644 --- a/src/generators/rust/constrainer/EnumConstrainer.ts +++ b/src/generators/rust/constrainer/EnumConstrainer.ts @@ -46,7 +46,7 @@ export function defaultEnumKeyConstraints(customConstraints?: Partial { + return ({ enumValue }) => { return enumValue; }; } diff --git a/src/generators/typescript/TypeScriptFileGenerator.ts b/src/generators/typescript/TypeScriptFileGenerator.ts index 183e73c0b0..e02ecacb5b 100644 --- a/src/generators/typescript/TypeScriptFileGenerator.ts +++ b/src/generators/typescript/TypeScriptFileGenerator.ts @@ -11,14 +11,15 @@ export class TypeScriptFileGenerator extends TypeScriptGenerator implements Abst * @param input * @param outputDirectory where you want the models generated to * @param options + * @param ensureFilesWritten verify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options?: TypeScriptRenderCompleteModelOptions): Promise { + public async generateToFiles(input: Record | InputMetaModel, outputDirectory: string, options?: TypeScriptRenderCompleteModelOptions, ensureFilesWritten = false): Promise { let generatedModels = await this.generateCompleteModels(input, options || {}); //Filter anything out that have not been successfully generated generatedModels = generatedModels.filter((outputModel) => { return outputModel.modelName !== ''; }); for (const outputModel of generatedModels) { const filePath = path.resolve(outputDirectory, `${outputModel.modelName}.ts`); - await FileHelpers.writerToFileSystem(outputModel.result, filePath); + await FileHelpers.writerToFileSystem(outputModel.result, filePath, ensureFilesWritten); } return generatedModels; } diff --git a/src/generators/typescript/TypeScriptGenerator.ts b/src/generators/typescript/TypeScriptGenerator.ts index b854964e52..539f7da399 100644 --- a/src/generators/typescript/TypeScriptGenerator.ts +++ b/src/generators/typescript/TypeScriptGenerator.ts @@ -4,7 +4,7 @@ import { defaultGeneratorOptions } from '../AbstractGenerator'; import { ConstrainedEnumModel, ConstrainedMetaModel, ConstrainedObjectModel, InputMetaModel, MetaModel, RenderOutput } from '../../models'; -import { constrainMetaModel, Constraints, split, TypeMapping, hasPreset, renderJavaScriptDependency } from '../../helpers'; +import { constrainMetaModel, Constraints, split, TypeMapping, hasPreset, SplitOptions, renderJavaScriptDependency } from '../../helpers'; import { TypeScriptPreset, TS_DEFAULT_PRESET } from './TypeScriptPreset'; import { ClassRenderer } from './renderers/ClassRenderer'; import { InterfaceRenderer } from './renderers/InterfaceRenderer'; @@ -31,7 +31,7 @@ export interface TypeScriptRenderCompleteModelOptions { /** * Generator for TypeScript */ -export class TypeScriptGenerator extends AbstractGenerator { +export class TypeScriptGenerator extends AbstractGenerator { static defaultOptions: TypeScriptOptions = { ...defaultGeneratorOptions, renderTypes: true, @@ -53,7 +53,7 @@ export class TypeScriptGenerator extends AbstractGenerator = new Map()): MetaModel { const hasModel = alreadySeenModels.has(jsonSchemaModel); @@ -9,6 +8,10 @@ export function convertToMetaModel(jsonSchemaModel: CommonModel, alreadySeenMode } const modelName = jsonSchemaModel.$id || 'undefined'; + const unionModel = convertToUnionModel(jsonSchemaModel, modelName, alreadySeenModels); + if (unionModel !== undefined) { + return unionModel; + } const anyModel = convertToAnyModel(jsonSchemaModel, modelName); if (anyModel !== undefined) { return anyModel; @@ -21,6 +24,10 @@ export function convertToMetaModel(jsonSchemaModel: CommonModel, alreadySeenMode if (objectModel !== undefined) { return objectModel; } + const dictionaryModel = convertToDictionaryModel(jsonSchemaModel, modelName, alreadySeenModels); + if (dictionaryModel !== undefined) { + return dictionaryModel; + } const tupleModel = convertToTupleModel(jsonSchemaModel, modelName, alreadySeenModels); if (tupleModel !== undefined) { return tupleModel; @@ -29,10 +36,6 @@ export function convertToMetaModel(jsonSchemaModel: CommonModel, alreadySeenMode if (arrayModel !== undefined) { return arrayModel; } - const unionModel = convertToUnionModel(jsonSchemaModel, modelName, alreadySeenModels); - if (unionModel !== undefined) { - return unionModel; - } const stringModel = convertToStringModel(jsonSchemaModel, modelName); if (stringModel !== undefined) { return stringModel; @@ -52,14 +55,34 @@ export function convertToMetaModel(jsonSchemaModel: CommonModel, alreadySeenMode Logger.error('Failed to convert to MetaModel, defaulting to AnyModel'); return new AnyModel(modelName, jsonSchemaModel.originalInput); } +function isEnumModel(jsonSchemaModel: CommonModel): boolean { + if (!Array.isArray(jsonSchemaModel.enum)) { + return false; + } + return true; +} + +/** + * Converts a CommonModel into multiple models wrapped in a union model. + * + * Because a CommonModel might contain multiple models, it's name for each of those models would be the same, instead we slightly change the model name. + * Each model has it's type as a name prepended to the union name. + * + * If the CommonModel has multiple types + */ +// eslint-disable-next-line sonarjs/cognitive-complexity export function convertToUnionModel(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map): UnionModel | undefined { const containsUnions = Array.isArray(jsonSchemaModel.union); const containsSimpleTypeUnion = Array.isArray(jsonSchemaModel.type) && jsonSchemaModel.type.length > 1; - if (!containsSimpleTypeUnion && !containsUnions) { + const containsAllTypes = Array.isArray(jsonSchemaModel.type) && jsonSchemaModel.type.length === 7; + if (!containsSimpleTypeUnion && !containsUnions || isEnumModel(jsonSchemaModel) || containsAllTypes) { return undefined; } const unionModel = new UnionModel(name, jsonSchemaModel.originalInput, []); - alreadySeenModels.set(jsonSchemaModel, unionModel); + //cache model before continuing + if (!alreadySeenModels.has(jsonSchemaModel)) { + alreadySeenModels.set(jsonSchemaModel, unionModel); + } // Has multiple types, so convert to union if (containsUnions && jsonSchemaModel.union) { @@ -68,37 +91,43 @@ export function convertToUnionModel(jsonSchemaModel: CommonModel, name: string, unionModel.union.push(unionMetaModel); } return unionModel; - } + } + // Has simple union types - const enumModel = convertToEnumModel(jsonSchemaModel, name); + // Each must have a different name then the root union model, as it otherwise clashes when code is generated + const enumModel = convertToEnumModel(jsonSchemaModel, `${name}_enum`); if (enumModel !== undefined) { unionModel.union.push(enumModel); } - const objectModel = convertToObjectModel(jsonSchemaModel, name, new Map()); + const objectModel = convertToObjectModel(jsonSchemaModel, `${name}_object`, alreadySeenModels); if (objectModel !== undefined) { unionModel.union.push(objectModel); } - const tupleModel = convertToTupleModel(jsonSchemaModel, name, alreadySeenModels); + const dictionaryModel = convertToDictionaryModel(jsonSchemaModel, `${name}_dictionary`, alreadySeenModels); + if (dictionaryModel !== undefined) { + unionModel.union.push(dictionaryModel); + } + const tupleModel = convertToTupleModel(jsonSchemaModel, `${name}_tuple`, alreadySeenModels); if (tupleModel !== undefined) { unionModel.union.push(tupleModel); } - const arrayModel = convertToArrayModel(jsonSchemaModel, name, alreadySeenModels); + const arrayModel = convertToArrayModel(jsonSchemaModel, `${name}_array`, alreadySeenModels); if (arrayModel !== undefined) { unionModel.union.push(arrayModel); } - const stringModel = convertToStringModel(jsonSchemaModel, name); + const stringModel = convertToStringModel(jsonSchemaModel, `${name}_string`); if (stringModel !== undefined) { unionModel.union.push(stringModel); } - const floatModel = convertToFloatModel(jsonSchemaModel, name); + const floatModel = convertToFloatModel(jsonSchemaModel, `${name}_float`); if (floatModel !== undefined) { unionModel.union.push(floatModel); } - const integerModel = convertToIntegerModel(jsonSchemaModel, name); + const integerModel = convertToIntegerModel(jsonSchemaModel, `${name}_integer`); if (integerModel !== undefined) { unionModel.union.push(integerModel); } - const booleanModel = convertToBooleanModel(jsonSchemaModel, name); + const booleanModel = convertToBooleanModel(jsonSchemaModel, `${name}_boolean`); if (booleanModel !== undefined) { unionModel.union.push(booleanModel); } @@ -129,11 +158,13 @@ export function convertToFloatModel(jsonSchemaModel: CommonModel, name: string): return new FloatModel(name, jsonSchemaModel.originalInput); } export function convertToEnumModel(jsonSchemaModel: CommonModel, name: string): EnumModel | undefined { - if (!Array.isArray(jsonSchemaModel.enum)) { + if (!isEnumModel(jsonSchemaModel)) { return undefined; } const metaModel = new EnumModel(name, jsonSchemaModel.originalInput, []); - for (const enumValue of jsonSchemaModel.enum) { + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + for (const enumValue of jsonSchemaModel.enum!) { let enumKey = enumValue; if (typeof enumValue !== 'string') { enumKey = JSON.stringify(enumValue); @@ -149,13 +180,38 @@ export function convertToBooleanModel(jsonSchemaModel: CommonModel, name: string } return new BooleanModel(name, jsonSchemaModel.originalInput); } +/** + * Determine whether we have a dictionary or an object. because in some cases inputs might be: + * { "type": "object", "additionalProperties": { "$ref": "#" } } which is to be interpreted as a dictionary not an object model. + */ +function isDictionary(jsonSchemaModel: CommonModel): boolean { + if (Object.keys(jsonSchemaModel.properties || {}).length > 0 || jsonSchemaModel.additionalProperties === undefined) { + return false; + } + return true; +} +export function convertToDictionaryModel(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map): DictionaryModel | undefined { + if (!isDictionary(jsonSchemaModel)) { + return undefined; + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const keyModel = new StringModel(name, jsonSchemaModel.additionalProperties!.originalInput); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const valueModel = convertToMetaModel(jsonSchemaModel.additionalProperties!, alreadySeenModels); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return new DictionaryModel(name, jsonSchemaModel.additionalProperties!.originalInput, keyModel, valueModel, 'normal'); +} export function convertToObjectModel(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map): ObjectModel | undefined { - if (!jsonSchemaModel.type?.includes('object')) { + if (!jsonSchemaModel.type?.includes('object') || + isDictionary(jsonSchemaModel)) { return undefined; } + const metaModel = new ObjectModel(name, jsonSchemaModel.originalInput, {}); - //cache model before continuing - alreadySeenModels.set(jsonSchemaModel, metaModel); + //cache model before continuing + if (!alreadySeenModels.has(jsonSchemaModel)) { + alreadySeenModels.set(jsonSchemaModel, metaModel); + } for (const [propertyName, prop] of Object.entries(jsonSchemaModel.properties || {})) { const isRequired = jsonSchemaModel.isRequired(propertyName); diff --git a/src/helpers/FileHelpers.ts b/src/helpers/FileHelpers.ts index c8997d6244..e7e8fd4fa6 100644 --- a/src/helpers/FileHelpers.ts +++ b/src/helpers/FileHelpers.ts @@ -1,5 +1,15 @@ -import * as fs from 'fs'; +import { promises as fs } from 'fs'; import * as path from 'path'; + +/** + * Convert a string into utf-8 encoding and return the byte size. + */ +function lengthInUtf8Bytes(str: string): number { + // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence. + const m = encodeURIComponent(str).match(/%[89ABab]/g); + return str.length + (m ? m.length : 0); +} + export class FileHelpers { /** * Node specific file writer, which writes the content to the specified filepath. @@ -7,13 +17,47 @@ export class FileHelpers { * This function is invasive, as it overwrite any existing files with the same name as the model. * * @param content to write - * @param filePath to write to + * @param filePath to write to, + * @param ensureFilesWritten veryify that the files is completely written before returning, this can happen if the file system is swamped with write requests. */ - static async writerToFileSystem(content: string, filePath: string): Promise { - const outputFilePath = path.resolve(filePath); - // eslint-disable-next-line security/detect-non-literal-fs-filename - await fs.promises.mkdir(path.dirname(outputFilePath), { recursive: true }); - // eslint-disable-next-line security/detect-non-literal-fs-filename - await fs.promises.writeFile(outputFilePath, content); + static writerToFileSystem(content: string, filePath: string, ensureFilesWritten = false): Promise { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + try { + const outputFilePath = path.resolve(filePath); + // eslint-disable-next-line security/detect-non-literal-fs-filename + await fs.mkdir(path.dirname(outputFilePath), { recursive: true }); + // eslint-disable-next-line security/detect-non-literal-fs-filename + await fs.writeFile(outputFilePath, content); + + /** + * It happens that the promise is resolved before the file is actually written to. + * + * This often happen if the file system is swamped with write requests in either benchmarks or in our blackbox tests. + * + * To avoid this we dont resolve until we are sure the file is written and exists. + */ + if (ensureFilesWritten) { + // eslint-disable-next-line no-undef + const timerId = setInterval(async () => { + try { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const isExists = await fs.stat(outputFilePath); + if (isExists && isExists.size === lengthInUtf8Bytes(content)) { + // eslint-disable-next-line no-undef + clearInterval(timerId); + resolve(); + } + } catch (e) { + // Ignore errors here as the file might not have been written yet + } + }, 10); + } else { + resolve(); + } + } catch (e) { + reject(e); + } + }); } } diff --git a/test/blackbox/BlackBoxTestFiles.ts b/test/blackbox/BlackBoxTestFiles.ts new file mode 100644 index 0000000000..b60e1461ed --- /dev/null +++ b/test/blackbox/BlackBoxTestFiles.ts @@ -0,0 +1,78 @@ +import * as path from 'path'; +import * as fs from 'fs'; + +/** + * Read all the files in the folder, and return the appropriate Jest `each` entries. + * @param folder + */ +function readFilesInFolder(folder: string) { + // eslint-disable-next-line no-undef + const fullPath = path.resolve(__dirname, `./docs/${folder}`); + // eslint-disable-next-line security/detect-non-literal-fs-filename + return fs.readdirSync(fullPath).map( + (file) => { + return { file: `./docs/${folder}/${file}`, outputDirectory: `./output/${folder}/${path.parse(file).name}` }; + } + ); +} + +const OpenAPI3_0Files = readFilesInFolder('OpenAPI-3_0'); +const jsonSchemaDraft7Files = readFilesInFolder('JsonSchemaDraft-7'); +const jsonSchemaDraft6Files = readFilesInFolder('JsonSchemaDraft-6'); +const jsonSchemaDraft4Files = readFilesInFolder('JsonSchemaDraft-4'); +const AsyncAPIV2_0Files = readFilesInFolder('AsyncAPI-2_0'); +const AsyncAPIV2_1Files = readFilesInFolder('AsyncAPI-2_1'); +const AsyncAPIV2_2Files = readFilesInFolder('AsyncAPI-2_2'); +const AsyncAPIV2_3Files = readFilesInFolder('AsyncAPI-2_3'); +const AsyncAPIV2_4Files = readFilesInFolder('AsyncAPI-2_4'); +const AsyncAPIV2_5Files = readFilesInFolder('AsyncAPI-2_5'); + +const filesToTest = [ + ...OpenAPI3_0Files.filter(({ file }) => { + // Too large to process in normal blackbox testing, can be used to locally test stuff. + return !file.includes('postman-api.json'); + }).filter(({ file }) => { + // Too large to process in normal blackbox testing, can be used to locally test stuff. + return !file.includes('twilio-1_13.json'); + }), + ...AsyncAPIV2_0Files.filter(({ file }) => { + // Too large to process in normal blackbox testing, can be used to locally test stuff. + return !file.includes('zbos_mqtt-all-asyncapi.json'); + }), + ...AsyncAPIV2_1Files, + ...AsyncAPIV2_2Files, + ...AsyncAPIV2_3Files, + ...AsyncAPIV2_4Files, + ...AsyncAPIV2_5Files, + ...jsonSchemaDraft4Files.filter(({ file }) => { + // Too large to process in normal blackbox testing, can be used lto ocally test stuff. + return !file.includes('aws-cloudformation.json'); + }), + ...jsonSchemaDraft7Files.filter(({ file }) => { + // Too large to process in normal blackbox testing, can be used to locally test stuff. + return !file.includes('graphql-code-generator.json'); + }), + ...jsonSchemaDraft6Files.filter(({ file }) => { + // Too large to process in normal blackbox testing, can be used to locally test stuff. + return !file.includes('fhir-full.json'); + }), +]; + +/** + * Officially only use one specific file for each input type, and the rest is for local testing. + * + * Otherwise the CI system will take far too long. + */ +export default filesToTest.filter(({ file }) => { + return file.includes('AsyncAPI-2_0/dummy.json') || + file.includes('AsyncAPI-2_1/dummy.json') || + file.includes('AsyncAPI-2_2/dummy.json') || + file.includes('AsyncAPI-2_3/dummy.json') || + file.includes('AsyncAPI-2_4/dummy.json') || + file.includes('AsyncAPI-2_5/streetlight_kafka.json') || + file.includes('JsonSchemaDraft-4/draft-4-core.json') || + file.includes('JsonSchemaDraft-6/draft-6-core.json') || + file.includes('JsonSchemaDraft-7/draft-7-core.json') || + file.includes('OpenAPI-3_0/petstore.json') || + file.includes('Swagger-2_0/petstore.json'); +}); diff --git a/test/blackbox/README.md b/test/blackbox/README.md index 6db9130b9e..489f6f2a85 100644 --- a/test/blackbox/README.md +++ b/test/blackbox/README.md @@ -8,24 +8,26 @@ The documents being tested can be found under [docs](./docs), which contain docu - [AsyncAPI 2.2](./docs/AsyncAPI-2_2) - [AsyncAPI 2.3](./docs/AsyncAPI-2_3) - [AsyncAPI 2.4](./docs/AsyncAPI-2_4) +- [AsyncAPI 2.5](./docs/AsyncAPI-2_5) - [JSON Schema draft 4](./docs/JsonSchemaDraft-4) - [JSON Schema draft 6](./docs/JsonSchemaDraft-6) - [JSON Schema draft 7](./docs/JsonSchemaDraft-7) - [Swagger 2.0](./docs/Swagger-2_0) - [OpenAPI 3.0](./docs/OpenAPI-3_0) -Each document is tested across all output languages and output will be written to `./output` folder in appropriate sub folders, for easier access. +Each document is tested across all output languages and output will be written to `./output` folder in appropriate sub-folders, for easier access. ## Running the tests -The tests can either be run by installing all dependencies locally, or running it through docker. - -To run the BlackBox tests through Docker, run the command `npm run docker:test:blackbox`. +The tests can either be run by installing all dependencies locally or running it through docker. If you want to run the BlackBox tests locally, you have to install a couple of dependencies: -- To to run the `Java` BlackBox tests, you need to have JDK installed. -- To to run the `TypeScript` 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/ -- To to run the `Go` BlackBox tests, you need to have GoLang installed - https://golang.org/doc/install -- To to run the `Python` BlackBox tests, you need to have python installed - https://www.python.org/downloads/ +- To run the `Java` BlackBox tests, you need to have JDK installed. +- To run the `TypeScript` BlackBox tests, you need to have TypeScript installed globally - `npm install -g typescript`. +- To run the `C#` BlackBox tests, you need to have C# compiler installed globally. - https://www.mono-project.com/download/stable/ +- To run the `Go` BlackBox tests, you need to have GoLang installed - https://golang.org/doc/install +- To run the `Python` BlackBox tests, you need to have python installed - https://www.python.org/downloads/ +- To run the `Rust` BlackBox tests, you need to have rust installed - https://www.rust-lang.org/tools/install (if you are on mac you might also need to install xcode `xcode-select --install`) -By default, the BlackBox tests are not run with the regular `npm run test`, but can be run with `npm run test:blackbox`. +By default, the BlackBox tests are not run with the regular `npm run test`, but can be run with `npm run test:blackbox`. Or run individual BlackBox tests you can run the commands `npm run test:blackbox:${language}` where language is one of `csharp`, `go`, `java`, `javascript`, `python`, `rust`, `typescript`, etc. + +To run the BlackBox tests through Docker, run the command `npm run docker:test:blackbox`. diff --git a/test/blackbox/blackbox-csharp.spec.ts b/test/blackbox/blackbox-csharp.spec.ts new file mode 100644 index 0000000000..80c471cc26 --- /dev/null +++ b/test/blackbox/blackbox-csharp.spec.ts @@ -0,0 +1,40 @@ +/** + * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. + * + * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. + * + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { CSharpFileGenerator, InputMetaModel } from '../../src'; +import { execCommand } from './utils/Utils'; +import filesToTest from './BlackBoxTestFiles'; +describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + jest.setTimeout(1000000); + const fileToGenerateFor = path.resolve(__dirname, file); + const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'csharp'); + let models: InputMetaModel; + beforeAll(async () => { + if (fs.existsSync(outputDirectoryPath)) { + fs.rmSync(outputDirectoryPath, { recursive: true }); + } + const inputFileContent = await fs.promises.readFile(fileToGenerateFor); + const generator = new CSharpFileGenerator(); + const input = JSON.parse(String(inputFileContent)); + models = await generator.process(input); + }); + describe(file, () => { + describe('should be able to generate and compile C#', () => { + test('class and enums', async () => { + const generator = new CSharpFileGenerator(); + + const generatedModels = await generator.generateToFiles(models, outputDirectoryPath, { namespace: 'TestNamespace' }); + expect(generatedModels).not.toHaveLength(0); + + const compileCommand = `csc /target:library /out:${path.resolve(outputDirectoryPath, './compiled.dll')} ${path.resolve(outputDirectoryPath, '*.cs')}`; + await execCommand(compileCommand); + }); + }); + }); +}); diff --git a/test/blackbox/blackbox-go.spec.ts b/test/blackbox/blackbox-go.spec.ts new file mode 100644 index 0000000000..d43a1dd315 --- /dev/null +++ b/test/blackbox/blackbox-go.spec.ts @@ -0,0 +1,42 @@ +/** + * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. + * + * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. + * + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { GoFileGenerator, InputMetaModel, InputProcessor } from '../../src'; +import { execCommand } from './utils/Utils'; +import filesToTest from './BlackBoxTestFiles'; + +describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + jest.setTimeout(1000000); + const fileToGenerateFor = path.resolve(__dirname, file); + const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'go'); + let models: InputMetaModel; + beforeAll(async () => { + if (fs.existsSync(outputDirectoryPath)) { + fs.rmSync(outputDirectoryPath, { recursive: true }); + } + const inputFileContent = await fs.promises.readFile(fileToGenerateFor); + const processor = new InputProcessor(); + const input = JSON.parse(String(inputFileContent)); + models = await processor.process(input); + }); + describe(file, () => { + describe('should be able to generate Go', () => { + test('struct', async () => { + const generator = new GoFileGenerator(); + const renderOutputPath = path.resolve(outputDirectoryPath, './struct/'); + + const generatedModels = await generator.generateToFiles(models, renderOutputPath, { packageName: 'test_package_name' }); + expect(generatedModels).not.toHaveLength(0); + + const compileCommand = `gofmt ${renderOutputPath}`; + await execCommand(compileCommand); + }); + }); + }); +}); diff --git a/test/blackbox/blackbox-java.spec.ts b/test/blackbox/blackbox-java.spec.ts new file mode 100644 index 0000000000..0e5b641acf --- /dev/null +++ b/test/blackbox/blackbox-java.spec.ts @@ -0,0 +1,58 @@ +/** + * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. + * + * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. + * + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { InputMetaModel, InputProcessor, JavaFileGenerator, JAVA_COMMON_PRESET } from '../../src'; +import { execCommand } from './utils/Utils'; +import filesToTest from './BlackBoxTestFiles'; + +describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + jest.setTimeout(1000000); + const fileToGenerateFor = path.resolve(__dirname, file); + const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'java'); + let models: InputMetaModel; + beforeAll(async () => { + if (fs.existsSync(outputDirectoryPath)) { + fs.rmSync(outputDirectoryPath, { recursive: true }); + } + const inputFileContent = await fs.promises.readFile(fileToGenerateFor); + const processor = new InputProcessor(); + const input = JSON.parse(String(inputFileContent)); + models = await processor.process(input); + }); + describe(file, () => { + const javaGeneratorOptions = [ + { + generatorOption: {}, + description: 'default generator', + renderOutputPath: path.resolve(outputDirectoryPath, './class/default') + }, + { + generatorOption: { + presets: [ + JAVA_COMMON_PRESET + ] + }, + description: 'all common presets', + renderOutputPath: path.resolve(outputDirectoryPath, './class/commonpreset') + } + ]; + describe.each(javaGeneratorOptions)('should be able to generate and compile Java', ({ generatorOption, renderOutputPath }) => { + test('class and enums', async () => { + const generator = new JavaFileGenerator(generatorOption); + const dependencyPath = path.resolve(__dirname, './dependencies/java/*'); + + const generatedModels = await generator.generateToFiles(models, renderOutputPath, { packageName: 'TestPackageName' }); + expect(generatedModels).not.toHaveLength(0); + + const compileCommand = `javac -cp ${dependencyPath} ${path.resolve(renderOutputPath, '*.java')}`; + await execCommand(compileCommand); + }); + }); + }); +}); diff --git a/test/blackbox/blackbox-javascript.spec.ts b/test/blackbox/blackbox-javascript.spec.ts new file mode 100644 index 0000000000..d13f2c9392 --- /dev/null +++ b/test/blackbox/blackbox-javascript.spec.ts @@ -0,0 +1,45 @@ +/** + * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. + * + * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. + * + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { InputMetaModel, InputProcessor, JavaScriptFileGenerator } from '../../src'; +import { execCommand } from './utils/Utils'; +import filesToTest from './BlackBoxTestFiles'; + +describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + jest.setTimeout(1000000); + const fileToGenerateFor = path.resolve(__dirname, file); + const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'js'); + let models: InputMetaModel; + beforeAll(async () => { + if (fs.existsSync(outputDirectoryPath)) { + fs.rmSync(outputDirectoryPath, { recursive: true }); + } + const inputFileContent = await fs.promises.readFile(fileToGenerateFor); + const processor = new InputProcessor(); + const input = JSON.parse(String(inputFileContent)); + models = await processor.process(input); + }); + describe(file, () => { + describe('should be able to generate JS', () => { + test('class', async () => { + const generator = new JavaScriptFileGenerator({moduleSystem: 'CJS'}); + const renderOutputPath = path.resolve(outputDirectoryPath, './class'); + + const generatedModels = await generator.generateToFiles(models, renderOutputPath, { }, true); + expect(generatedModels).not.toHaveLength(0); + + const files = fs.readdirSync(renderOutputPath); + for (const file of files) { + const transpileAndRunCommand = `node --check ${path.resolve(renderOutputPath, file)}`; + await execCommand(transpileAndRunCommand); + } + }); + }); + }); +}); diff --git a/test/blackbox/blackbox-python.spec.ts b/test/blackbox/blackbox-python.spec.ts new file mode 100644 index 0000000000..1891b5e519 --- /dev/null +++ b/test/blackbox/blackbox-python.spec.ts @@ -0,0 +1,43 @@ +/** + * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. + * + * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. + * + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { InputMetaModel, InputProcessor, PythonFileGenerator, PythonRenderCompleteModelOptions } from '../../src'; +import { execCommand } from './utils/Utils'; +import filesToTest from './BlackBoxTestFiles'; + +describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + jest.setTimeout(1000000); + const fileToGenerateFor = path.resolve(__dirname, file); + const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'python'); + let models: InputMetaModel; + beforeAll(async () => { + if (fs.existsSync(outputDirectoryPath)) { + fs.rmSync(outputDirectoryPath, { recursive: true }); + } + const inputFileContent = await fs.promises.readFile(fileToGenerateFor); + const processor = new InputProcessor(); + const input = JSON.parse(String(inputFileContent)); + models = await processor.process(input); + }); + describe(file, () => { + describe('should be able to generate Python', () => { + test('class and enums', async () => { + const generator = new PythonFileGenerator(); + const renderOutputPath = path.resolve(outputDirectoryPath, './class/'); + const options = { } as PythonRenderCompleteModelOptions; + const generatedModels = await generator.generateToFiles(models, renderOutputPath, options); + expect(generatedModels).not.toHaveLength(0); + + const compileCommand = `python -m compileall -f ${renderOutputPath}`; + await execCommand(compileCommand); + expect(generatedModels).not.toHaveLength(0); + }); + }); + }); +}); diff --git a/test/blackbox/blackbox-rust.spec.ts b/test/blackbox/blackbox-rust.spec.ts new file mode 100644 index 0000000000..1d7cec80fc --- /dev/null +++ b/test/blackbox/blackbox-rust.spec.ts @@ -0,0 +1,60 @@ +/** + * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. + * + * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. + * + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { defaultRustRenderCompleteModelOptions, InputMetaModel, InputProcessor, RustFileGenerator, RustPackageFeatures, RustRenderCompleteModelOptions } from '../../src'; +import { execCommand } from './utils/Utils'; +import filesToTest from './BlackBoxTestFiles'; + +describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + jest.setTimeout(1000000); + const fileToGenerateFor = path.resolve(__dirname, file); + const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'rust'); + let models: InputMetaModel; + beforeAll(async () => { + if (fs.existsSync(outputDirectoryPath)) { + fs.rmSync(outputDirectoryPath, { recursive: true }); + } + const inputFileContent = await fs.promises.readFile(fileToGenerateFor); + const processor = new InputProcessor(); + const input = JSON.parse(String(inputFileContent)); + models = await processor.process(input); + }); + describe(file, () => { + describe.skip('should be able to generate Rust', () => { + test('struct with serde_json', async () => { + const generator = new RustFileGenerator(); + const renderOutputPath = path.resolve(outputDirectoryPath, './struct'); + const cargoFile = path.resolve(outputDirectoryPath, './struct/Cargo.toml'); + const options = { + ...defaultRustRenderCompleteModelOptions, + supportFiles: true, // generate Cargo.toml and lib.rs + package: { + packageName: 'asyncapi-rs-example', + packageVersion: '1.0.0', + // set authors, homepage, repository, and license + authors: ['AsyncAPI Rust Champions'], + homepage: 'https://www.asyncapi.com/tools/modelina', + repository: 'https://github.com/asyncapi/modelina', + license: 'Apache-2.0', + description: 'Rust models generated by AsyncAPI Modelina', + // support 2018 editions and up + edition: '2018', + // enable serde_json + packageFeatures: [RustPackageFeatures.json] as RustPackageFeatures[] + } + } as RustRenderCompleteModelOptions; + const generatedModels = await generator.generateToPackage(models, renderOutputPath, options); + expect(generatedModels).not.toHaveLength(0); + + const compileCommand = `cargo build --manifest-path=${cargoFile}`; + await execCommand(compileCommand, true); + }); + }); + }); +}); diff --git a/test/blackbox/blackbox-typescript.spec.ts b/test/blackbox/blackbox-typescript.spec.ts new file mode 100644 index 0000000000..1d904af6e3 --- /dev/null +++ b/test/blackbox/blackbox-typescript.spec.ts @@ -0,0 +1,52 @@ +/** + * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. + * + * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. + * + */ +import * as path from 'path'; +import * as fs from 'fs'; +import { InputMetaModel, InputProcessor, TypeScriptFileGenerator } from '../../src'; +import { execCommand } from './utils/Utils'; +import filesToTest from './BlackBoxTestFiles'; + +describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { + jest.setTimeout(1000000); + const fileToGenerateFor = path.resolve(__dirname, file); + const outputDirectoryPath = path.resolve(__dirname, outputDirectory, 'ts'); + let models: InputMetaModel; + beforeAll(async () => { + if (fs.existsSync(outputDirectoryPath)) { + fs.rmSync(outputDirectoryPath, { recursive: true }); + } + const inputFileContent = await fs.promises.readFile(fileToGenerateFor); + const processor = new InputProcessor(); + const input = JSON.parse(String(inputFileContent)); + models = await processor.process(input); + }); + describe(file, () => { + describe('should be able to generate and transpile TS', () => { + test('class and enums', async () => { + const generator = new TypeScriptFileGenerator({ modelType: 'class' }); + const renderOutputPath = path.resolve(outputDirectoryPath, './class'); + + const generatedModels = await generator.generateToFiles(models, renderOutputPath, {}); + expect(generatedModels).not.toHaveLength(0); + + const transpileCommand = `tsc --downlevelIteration -t es5 --baseUrl ${renderOutputPath}`; + await execCommand(transpileCommand); + }); + + test('interface and enums', async () => { + const generator = new TypeScriptFileGenerator({ modelType: 'interface' }); + const renderOutputPath = path.resolve(outputDirectoryPath, './interface'); + + const generatedModels = await generator.generateToFiles(models, renderOutputPath, {}); + expect(generatedModels).not.toHaveLength(0); + + const transpileCommand = `tsc --downlevelIteration -t es5 --baseUrl ${renderOutputPath}`; + await execCommand(transpileCommand); + }); + }); + }); +}); diff --git a/test/blackbox/blackbox.spec.ts b/test/blackbox/blackbox.spec.ts deleted file mode 100644 index a89ff71d56..0000000000 --- a/test/blackbox/blackbox.spec.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Blackbox tests are the final line of defence, that takes different real-life example documents and generate their corresponding models in all supported languages. - * - * For those languages where it is possible, the models are compiled/transpiled to ensure there are no syntax errors in generated models. - * - */ - -import * as path from 'path'; -import * as fs from 'fs'; -import { RustFileGenerator, defaultRustRenderCompleteModelOptions, RustPackageFeatures, RustRenderCompleteModelOptions, GoFileGenerator, CSharpFileGenerator, JavaFileGenerator, JAVA_COMMON_PRESET, TypeScriptFileGenerator, JavaScriptFileGenerator, PythonFileGenerator, PythonRenderCompleteModelOptions } from '../../src'; -import { execCommand } from './utils/Utils'; - -/** - * Read all the files in the folder, and return the appropriate Jest `each` entries. - * @param folder - */ -function readFilesInFolder(folder: string) { - const fullPath = path.resolve(__dirname, `./docs/${folder}`); - return fs.readdirSync(fullPath).map( - (file) => { - return { file: `./docs/${folder}/${file}`, outputDirectory: `./output/${folder}/${path.parse(file).name}` }; - } - ); -} -const OpenAPI3_0Files = readFilesInFolder('OpenAPI-3_0'); -const jsonSchemaDraft7Files = readFilesInFolder('JsonSchemaDraft-7'); -const jsonSchemaDraft6Files = readFilesInFolder('JsonSchemaDraft-6'); -const jsonSchemaDraft4Files = readFilesInFolder('JsonSchemaDraft-4'); -const AsyncAPIV2_0Files = readFilesInFolder('AsyncAPI-2_0'); -const AsyncAPIV2_1Files = readFilesInFolder('AsyncAPI-2_1'); -const AsyncAPIV2_2Files = readFilesInFolder('AsyncAPI-2_2'); -const AsyncAPIV2_3Files = readFilesInFolder('AsyncAPI-2_3'); -const AsyncAPIV2_4Files = readFilesInFolder('AsyncAPI-2_4'); -const AsyncAPIV2_5Files = readFilesInFolder('AsyncAPI-2_5'); - -const filesToTest = [ - // ...OpenAPI3_0Files, - // ...AsyncAPIV2_0Files, - // ...AsyncAPIV2_1Files, - // ...AsyncAPIV2_2Files, - // ...AsyncAPIV2_3Files, - // ...AsyncAPIV2_4Files, - // ...AsyncAPIV2_5Files, - ...jsonSchemaDraft4Files.filter(({ file }) => { - // Too large to process https://github.com/asyncapi/modelina/issues/822 - return !file.includes('aws-cloudformation.json'); - }).filter(({ file }) => { - // Related to https://github.com/asyncapi/modelina/issues/389 - return !file.includes('jenkins-config.json'); - }).filter(({file}) => { - // Related to https://github.com/asyncapi/modelina/issues/840 - // Related to https://github.com/asyncapi/modelina/issues/841 - }).filter(({ file }) => { - // Related to https://github.com/asyncapi/modelina/issues/825 - return !file.includes('circleci-config.json'); - }), - // ...jsonSchemaDraft7Files, - // ...jsonSchemaDraft6Files, -]; - -// eslint-disable-next-line no-console -console.log('This is gonna take some time, Stay Awhile and Listen'); -describe.each(filesToTest)('Should be able to generate with inputs', ({ file, outputDirectory }) => { - jest.setTimeout(1000000); - const fileToGenerateFor = path.resolve(__dirname, file); - const outputDirectoryPath = path.resolve(__dirname, outputDirectory); - beforeAll(async () => { - if (fs.existsSync(outputDirectoryPath)) { - await fs.rmSync(outputDirectoryPath, { recursive: true }); - } - }); - describe(file, () => { - const javaGeneratorOptions = [ - { - generatorOption: {}, - description: 'default generator', - renderOutputPath: path.resolve(outputDirectoryPath, './java/class/default') - }, - { - generatorOption: { - presets: [ - JAVA_COMMON_PRESET - ] - }, - description: 'all common presets', - renderOutputPath: path.resolve(outputDirectoryPath, './java/class/commonpreset') - } - ]; - describe.each(javaGeneratorOptions)('should be able to generate and compile Java', ({ generatorOption, description, renderOutputPath }) => { - test('class and enums', async () => { - const generator = new JavaFileGenerator(generatorOption); - const inputFileContent = await fs.promises.readFile(fileToGenerateFor); - const input = JSON.parse(String(inputFileContent)); - const dependencyPath = path.resolve(__dirname, './dependencies/java/*'); - - const generatedModels = await generator.generateToFiles(input, renderOutputPath, { packageName: 'TestPackageName' }); - expect(generatedModels).not.toHaveLength(0); - - const compileCommand = `javac -cp ${dependencyPath} ${path.resolve(renderOutputPath, '*.java')}`; - await execCommand(compileCommand); - }); - }); - describe('should be able to generate and compile C#', () => { - test('class and enums', async () => { - const generator = new CSharpFileGenerator(); - const inputFileContent = await fs.promises.readFile(fileToGenerateFor); - const input = JSON.parse(String(inputFileContent)); - const renderOutputPath = path.resolve(outputDirectoryPath, './csharp'); - - const generatedModels = await generator.generateToFiles(input, renderOutputPath, { namespace: 'TestNamespace' }); - expect(generatedModels).not.toHaveLength(0); - - const compileCommand = `csc /target:library /out:${path.resolve(renderOutputPath, './compiled.dll')} ${path.resolve(renderOutputPath, '*.cs')}`; - await execCommand(compileCommand); - }); - }); - - describe('should be able to generate and transpile TS', () => { - test('class and enums', async () => { - const generator = new TypeScriptFileGenerator({ modelType: 'class' }); - const inputFileContent = await fs.promises.readFile(fileToGenerateFor); - const input = JSON.parse(String(inputFileContent)); - const renderOutputPath = path.resolve(outputDirectoryPath, './ts/class'); - - const generatedModels = await generator.generateToFiles(input, renderOutputPath); - expect(generatedModels).not.toHaveLength(0); - - const transpileCommand = `tsc --downlevelIteration -t es5 --baseUrl ${renderOutputPath}`; - await execCommand(transpileCommand); - }); - - test('interface and enums', async () => { - const generator = new TypeScriptFileGenerator({ modelType: 'interface' }); - const inputFileContent = await fs.promises.readFile(fileToGenerateFor); - const input = JSON.parse(String(inputFileContent)); - const renderOutputPath = path.resolve(outputDirectoryPath, './ts/interface'); - - const generatedModels = await generator.generateToFiles(input, renderOutputPath); - expect(generatedModels).not.toHaveLength(0); - - const transpileCommand = `tsc --downlevelIteration -t es5 --baseUrl ${renderOutputPath}`; - await execCommand(transpileCommand); - }); - }); - - describe('should be able to generate JS', () => { - test('class', async () => { - const generator = new JavaScriptFileGenerator(); - const inputFileContent = await fs.promises.readFile(fileToGenerateFor); - const input = JSON.parse(String(inputFileContent)); - const renderOutputPath = path.resolve(outputDirectoryPath, './js/class'); - - const generatedModels = await generator.generateToFiles(input, renderOutputPath, { moduleSystem: 'CJS' }); - expect(generatedModels).not.toHaveLength(0); - - const files = fs.readdirSync(renderOutputPath); - for (const file of files) { - const transpileAndRunCommand = `node --check ${path.resolve(renderOutputPath, file)}`; - await execCommand(transpileAndRunCommand); - } - }); - }); - - describe('should be able to generate Go', () => { - test('struct', async () => { - const generator = new GoFileGenerator(); - const inputFileContent = await fs.promises.readFile(fileToGenerateFor); - const input = JSON.parse(String(inputFileContent)); - const renderOutputPath = path.resolve(outputDirectoryPath, './go/struct/'); - - const generatedModels = await generator.generateToFiles(input, renderOutputPath, { packageName: 'test_package_name' }); - expect(generatedModels).not.toHaveLength(0); - - const compileCommand = `gofmt ${renderOutputPath}`; - await execCommand(compileCommand); - }); - }); - describe('should be able to generate Rust', () => { - test('struct with serde_json', async () => { - const generator = new RustFileGenerator(); - const inputFileContent = await fs.promises.readFile(fileToGenerateFor); - const input = JSON.parse(String(inputFileContent)); - const renderOutputPath = path.resolve(outputDirectoryPath, './rust/struct/'); - const options = { - ...defaultRustRenderCompleteModelOptions, - supportFiles: true, // generate Cargo.toml and lib.rs - package: { - packageName: 'asyncapi-rs-example', - packageVersion: '1.0.0', - // set authors, homepage, repository, and license - authors: ['AsyncAPI Rust Champions'], - homepage: 'https://www.asyncapi.com/tools/modelina', - repository: 'https://github.com/asyncapi/modelina', - license: 'Apache-2.0', - description: 'Rust models generated by AsyncAPI Modelina', - // support 2018 editions and up - edition: '2018', - // enable serde_json - packageFeatures: [RustPackageFeatures.json] as RustPackageFeatures[] - } - } as RustRenderCompleteModelOptions; - const generatedModels = await generator.generateToPackage(input, renderOutputPath, options); - expect(generatedModels).not.toHaveLength(0); - - const compileCommand = `cargo build ${renderOutputPath}`; - await execCommand(compileCommand); - }); - }); - describe('should be able to generate Python', () => { - test('class and enums', async () => { - const generator = new PythonFileGenerator(); - const inputFileContent = await fs.promises.readFile(fileToGenerateFor); - const input = JSON.parse(String(inputFileContent)); - const renderOutputPath = path.resolve(outputDirectoryPath, './python/class/'); - const options = { } as PythonRenderCompleteModelOptions; - const generatedModels = await generator.generateToFiles(input, renderOutputPath, options); - expect(generatedModels).not.toHaveLength(0); - - const compileCommand = `python -m compileall -f ${renderOutputPath}`; - await execCommand(compileCommand); - expect(generatedModels).not.toHaveLength(0); - }); - }); - }); -}); diff --git a/test/blackbox/docs/JsonSchemaDraft-6/draft-6-core.json b/test/blackbox/docs/JsonSchemaDraft-6/draft-6-core.json new file mode 100644 index 0000000000..bd3e763bc2 --- /dev/null +++ b/test/blackbox/docs/JsonSchemaDraft-6/draft-6-core.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "examples": { + "type": "array", + "items": {} + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": {}, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": {} +} diff --git a/test/blackbox/utils/Utils.ts b/test/blackbox/utils/Utils.ts index 1246a35fb2..fa4989e682 100644 --- a/test/blackbox/utils/Utils.ts +++ b/test/blackbox/utils/Utils.ts @@ -23,16 +23,19 @@ export async function generateModels(absolutePathToFile: string, generator: Abst * * @param command */ -export async function execCommand(command: string) : Promise { +export async function execCommand(command: string, allowStdError = false) : Promise { try { const { stderr } = await promiseExec(command); if (stderr !== '') { - return Promise.reject(stderr); + if (!allowStdError) { + return Promise.reject(stderr); + } + // eslint-disable-next-line no-console + console.error(stderr); } return Promise.resolve(); } catch (e: any) { - const wrapperError = new Error(`Error: ${e.stack}; Stdout: ${e.stdout}`); - return Promise.reject(wrapperError); + return Promise.reject(`${e.stack}; Stdout: ${e.stdout}`); } } diff --git a/test/generators/FileGenerators.spec.ts b/test/generators/FileGenerators.spec.ts index 43f7a77b0a..3571d0ca35 100644 --- a/test/generators/FileGenerators.spec.ts +++ b/test/generators/FileGenerators.spec.ts @@ -79,6 +79,7 @@ describe.each(generatorsToTest)('generateToFiles', ({ generator, generatorOption const expectedWriteToFileParameters = [ 'content', expectedOutputFilePath, + false ]; jest.spyOn(FileHelpers, 'writerToFileSystem').mockResolvedValue(undefined); jest.spyOn(generator, 'generateCompleteModels').mockResolvedValue([new OutputModel('content', new ConstrainedAnyModel('', undefined, ''), 'test', new InputMetaModel(), [])]); diff --git a/test/generators/dart/presets/JsonPreset.spec.ts b/test/generators/dart/presets/JsonPreset.spec.ts index fce4b039e7..7e13a46de0 100644 --- a/test/generators/dart/presets/JsonPreset.spec.ts +++ b/test/generators/dart/presets/JsonPreset.spec.ts @@ -8,14 +8,6 @@ describe('DART_JSON_PRESET', () => { }); test('should render json annotations', async () => { - const doc = { - $id: 'Clazz', - type: 'object', - properties: { - min_number_prop: { type: 'number' }, - max_number_prop: { type: 'number' }, - }, - }; const model = new ConstrainedObjectModel('Clazz', undefined, 'Clazz', { minNumberProp: new ConstrainedObjectPropertyModel('minNumberProp', 'min_number_prop', false, new ConstrainedFloatModel('minNumberProp', undefined, 'double')), maxNumberProp: new ConstrainedObjectPropertyModel('maxNumberProp', 'max_number_prop', false, new ConstrainedFloatModel('maxNumberProp', undefined, 'double')) diff --git a/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap b/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap index 7b080e0af3..6b3b91c906 100644 --- a/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap +++ b/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap @@ -24,15 +24,6 @@ exports[`PythonGenerator Class should not render reserved keyword 1`] = ` " `; -exports[`PythonGenerator Class should work with empty objects 1`] = ` -"class CustomClass: - def __init__(self, input): - \\"\\"\\" - No properties - \\"\\"\\" -" -`; - exports[`PythonGenerator Class should render \`class\` type 1`] = ` "class Address: def __init__(self, input): @@ -139,6 +130,15 @@ exports[`PythonGenerator Class should work with custom preset for \`class\` type " `; +exports[`PythonGenerator Class should work with empty objects 1`] = ` +"class CustomClass: + def __init__(self, input): + \\"\\"\\" + No properties + \\"\\"\\" +" +`; + exports[`PythonGenerator Enum should render \`enum\` with mixed types (union type) 1`] = ` "class Things(Enum): TEXAS = \\"Texas\\" diff --git a/test/helpers/CommonModelToMetaModel.spec.ts b/test/helpers/CommonModelToMetaModel.spec.ts index 590a8560cf..d9340ffa26 100644 --- a/test/helpers/CommonModelToMetaModel.spec.ts +++ b/test/helpers/CommonModelToMetaModel.spec.ts @@ -210,7 +210,7 @@ describe('CommonModelToMetaModel', () => { expect(model instanceof UnionModel).toEqual(true); expect((model as UnionModel).union.length).toEqual(2); }); - test('should convert array of types to union model', () => { + test('should convert array of models to union model', () => { const cm = new CommonModel(); cm.$id = 'Pet'; const cat = new CommonModel(); diff --git a/test/helpers/DependencyHelpers.spec.ts b/test/helpers/DependencyHelpers.spec.ts index be9c10c941..66eadb7df4 100644 --- a/test/helpers/DependencyHelpers.spec.ts +++ b/test/helpers/DependencyHelpers.spec.ts @@ -1,4 +1,4 @@ -import { ConstrainedReferenceModel, ConstrainedStringModel } from '../../src'; +import { ConstrainedReferenceModel, ConstrainedStringModel, ConstrainedAnyModel } from '../../src'; import {renderJavaScriptDependency, makeUnique} from '../../src/helpers'; describe('DependencyHelper', () => { @@ -42,5 +42,13 @@ describe('DependencyHelper', () => { const uniqueArray = makeUnique(nonUniqueArray); expect(uniqueArray).toHaveLength(1); }); + test('should remove duplicate name and type models', () => { + const stringModel = new ConstrainedStringModel('', undefined, ''); + const ref = new ConstrainedReferenceModel('name', undefined, 'type', stringModel); + const any = new ConstrainedAnyModel('name', undefined, 'type'); + const nonUniqueArray = [ref, any]; + const uniqueArray = makeUnique(nonUniqueArray); + expect(uniqueArray).toHaveLength(1); + }); }); });