From dcaaee98566e37918143b6077d9c973a69f56f17 Mon Sep 17 00:00:00 2001 From: Qiaoqiao Zhang <55688292+qiaozha@users.noreply.github.com> Date: Tue, 17 Oct 2023 01:37:12 -0500 Subject: [PATCH] support additional properties in RLC (#2054) * try-fix-model-inherit-from-record * reserve work * fix record extends * fix modular literal error * fix ut error * fix ci * add more ut * resolve comments * use handle is record * fix lint error * fix ci failure --- packages/rlc-common/src/buildIndexFile.ts | 2 +- packages/rlc-common/src/buildObjectTypes.ts | 24 +- .../rlc-common/src/helpers/schemaHelpers.ts | 13 +- .../typespec-ts/review/authoring.api.md | 12 +- .../generated/typespec-ts/src/models.ts | 5 +- .../generated/typespec-ts/src/outputModels.ts | 5 +- .../typespec-ts/src/modular/emitModels.ts | 6 +- .../src/modular/helpers/typeHelpers.ts | 2 +- .../src/transform/transformSchemas.ts | 8 +- packages/typespec-ts/src/utils/modelUtils.ts | 283 +++++------ .../test/modularUnit/modelsGenerator.spec.ts | 70 ++- .../test/unit/modelsGenerator.spec.ts | 468 +++++++++++++++++- packages/typespec-ts/test/util/testUtil.ts | 7 + 13 files changed, 738 insertions(+), 167 deletions(-) diff --git a/packages/rlc-common/src/buildIndexFile.ts b/packages/rlc-common/src/buildIndexFile.ts index 634b16039c..ecda36757b 100644 --- a/packages/rlc-common/src/buildIndexFile.ts +++ b/packages/rlc-common/src/buildIndexFile.ts @@ -311,7 +311,7 @@ function generateRLCIndex(file: SourceFile, model: RLCModel) { hasMultiCollection(model) || hasSsvCollection(model) || hasPipeCollection(model) || - hasTsvCollection(model) || + hasTsvCollection(model) || hasCsvCollection(model) ) { file.addExportDeclarations([ diff --git a/packages/rlc-common/src/buildObjectTypes.ts b/packages/rlc-common/src/buildObjectTypes.ts index 052c20742e..9e505abb15 100644 --- a/packages/rlc-common/src/buildObjectTypes.ts +++ b/packages/rlc-common/src/buildObjectTypes.ts @@ -363,24 +363,32 @@ function getImmediateParentsNames( const extendFrom: string[] = []; - // If an immediate parent is a DictionarySchema, that means that the object has been marked + // If an immediate parent is an empty DictionarySchema, that means that the object has been marked // with additional properties. We need to add Record to the extend list and - if (objectSchema.parents.immediate.find(isDictionarySchema)) { + if ( + objectSchema.parents.immediate.find((im) => isDictionarySchema(im, {filterEmpty: true})) + ) { extendFrom.push("Record"); } // Get the rest of the parents excluding any DictionarySchemas const parents = objectSchema.parents.immediate - .filter((p) => !isDictionarySchema(p)) + .filter((p) => !isDictionarySchema(p, {filterEmpty: true})) .map((parent) => { const nameSuffix = schemaUsage.includes(SchemaContext.Output) ? "Output" : ""; - const name = `${normalizeName( - parent.name, - NameType.Interface, - true /** shouldGuard */ - )}${nameSuffix}`; + const name = isDictionarySchema(parent) + ? `${ + (schemaUsage.includes(SchemaContext.Output) + ? parent.outputTypeName + : parent.typeName) ?? parent.name + }` + : `${normalizeName( + parent.name, + NameType.Interface, + true /** shouldGuard */ + )}${nameSuffix}`; return isObjectSchema(parent) && isPolymorphicParent(parent) ? `${name}Parent` diff --git a/packages/rlc-common/src/helpers/schemaHelpers.ts b/packages/rlc-common/src/helpers/schemaHelpers.ts index 722c528372..2f68ef47c6 100644 --- a/packages/rlc-common/src/helpers/schemaHelpers.ts +++ b/packages/rlc-common/src/helpers/schemaHelpers.ts @@ -3,9 +3,18 @@ import { Schema } from "../interfaces.js"; -export function isDictionarySchema(schema: Schema) { +export interface IsDictionaryOptions { + filterEmpty?: boolean; +} + +export function isDictionarySchema( + schema: Schema, + options: IsDictionaryOptions = {} +) { if (schema.type === "dictionary") { - return true; + if (!options.filterEmpty || (options.filterEmpty && !schema.typeName)) { + return true; + } } return false; } diff --git a/packages/typespec-test/test/authoring/generated/typespec-ts/review/authoring.api.md b/packages/typespec-test/test/authoring/generated/typespec-ts/review/authoring.api.md index c31be791f5..7a7d63f447 100644 --- a/packages/typespec-test/test/authoring/generated/typespec-ts/review/authoring.api.md +++ b/packages/typespec-test/test/authoring/generated/typespec-ts/review/authoring.api.md @@ -759,7 +759,7 @@ export interface Project { language: string; multilingual?: boolean; projectKind: "CustomSingleLabelClassification" | "CustomMultiLabelClassification" | "CustomEntityRecognition"; - settings?: Record; + settings?: ProjectSettings; storageInputContainerName: string; } @@ -774,13 +774,21 @@ export interface ProjectOutput { multilingual?: boolean; projectKind: "CustomSingleLabelClassification" | "CustomMultiLabelClassification" | "CustomEntityRecognition"; readonly projectName: string; - settings?: Record; + settings?: ProjectSettingsOutput; storageInputContainerName: string; } // @public export type ProjectResourceMergeAndPatch = Partial; +// @public +export interface ProjectSettings extends Record { +} + +// @public +export interface ProjectSettingsOutput extends Record { +} + // @public (undocumented) export interface Routes { (path: "/authoring/analyze-text/projects/{projectName}", projectName: string): CreateOrUpdate; diff --git a/packages/typespec-test/test/authoring/generated/typespec-ts/src/models.ts b/packages/typespec-test/test/authoring/generated/typespec-ts/src/models.ts index e23a1b764c..d762986db1 100644 --- a/packages/typespec-test/test/authoring/generated/typespec-ts/src/models.ts +++ b/packages/typespec-test/test/authoring/generated/typespec-ts/src/models.ts @@ -11,7 +11,7 @@ export interface Project { /** The storage container name. */ storageInputContainerName: string; /** The project settings. */ - settings?: Record; + settings?: ProjectSettings; /** Whether the project would be used for multiple languages or not. */ multilingual?: boolean; /** The project description. */ @@ -20,6 +20,9 @@ export interface Project { language: string; } +/** Represents the settings used to define the project behavior. */ +export interface ProjectSettings extends Record {} + /** Training job parameters. */ export interface TrainingJobOptions { /** The model label. */ diff --git a/packages/typespec-test/test/authoring/generated/typespec-ts/src/outputModels.ts b/packages/typespec-test/test/authoring/generated/typespec-ts/src/outputModels.ts index 8e406ada31..6f37cf2ce2 100644 --- a/packages/typespec-test/test/authoring/generated/typespec-ts/src/outputModels.ts +++ b/packages/typespec-test/test/authoring/generated/typespec-ts/src/outputModels.ts @@ -16,7 +16,7 @@ export interface ProjectOutput { /** The storage container name. */ storageInputContainerName: string; /** The project settings. */ - settings?: Record; + settings?: ProjectSettingsOutput; /** Whether the project would be used for multiple languages or not. */ multilingual?: boolean; /** The project description. */ @@ -33,6 +33,9 @@ export interface ProjectOutput { readonly lastDeployedDateTime: string; } +/** Represents the settings used to define the project behavior. */ +export interface ProjectSettingsOutput extends Record {} + /** Provides status details for long running operations. */ export interface OperationStatusOutput { /** The unique ID of the operation. */ diff --git a/packages/typespec-ts/src/modular/emitModels.ts b/packages/typespec-ts/src/modular/emitModels.ts index 7254d33c79..ce48e1fb8f 100644 --- a/packages/typespec-ts/src/modular/emitModels.ts +++ b/packages/typespec-ts/src/modular/emitModels.ts @@ -139,7 +139,11 @@ export function buildModelsOptions( client: Client ) { const modelOptionsFile = codeModel.project.createSourceFile( - `${codeModel.modularOptions.sourceRoot}/${client.subfolder}/models/options.ts`, + path.join( + codeModel.modularOptions.sourceRoot, + client.subfolder ?? "", + `models/options.ts` + ), undefined, { overwrite: true diff --git a/packages/typespec-ts/src/modular/helpers/typeHelpers.ts b/packages/typespec-ts/src/modular/helpers/typeHelpers.ts index 017375b830..dc068a8bc7 100644 --- a/packages/typespec-ts/src/modular/helpers/typeHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/typeHelpers.ts @@ -38,7 +38,7 @@ export function getType(type: Type, format?: string): TypeMetadata { case "boolean": return { name: getNullableType(type.type, type) }; case "constant": { - let typeName: string = type.value ?? "undefined"; + let typeName: string = type.value?.toString() ?? "undefined"; if (type.valueType?.type === "string") { typeName = type.value ? `"${type.value}"` : "undefined"; } diff --git a/packages/typespec-ts/src/transform/transformSchemas.ts b/packages/typespec-ts/src/transform/transformSchemas.ts index f8253d1174..02d09df082 100644 --- a/packages/typespec-ts/src/transform/transformSchemas.ts +++ b/packages/typespec-ts/src/transform/transformSchemas.ts @@ -122,8 +122,12 @@ export function transformSchemas( setModelMap(model, context); const indexer = (model as Model).indexer; - if (indexer?.value && !program.stateMap(modelKey).get(indexer?.value)) { - setModelMap(indexer.value, context); + if ( + indexer?.value && + (!program.stateMap(modelKey).get(indexer?.value) || + !program.stateMap(modelKey).get(indexer?.value)?.includes(context)) + ) { + getGeneratedModels(indexer.value, context); } for (const prop of model.properties) { if ( diff --git a/packages/typespec-ts/src/utils/modelUtils.ts b/packages/typespec-ts/src/utils/modelUtils.ts index 7c4eb064f0..96fdc51fd0 100644 --- a/packages/typespec-ts/src/utils/modelUtils.ts +++ b/packages/typespec-ts/src/utils/modelUtils.ts @@ -41,7 +41,9 @@ import { listServices, Program, getEncode, - EncodeData + EncodeData, + isRecordModelType, + isArrayModelType } from "@typespec/compiler"; import { reportDiagnostic } from "../lib.js"; import { @@ -134,7 +136,7 @@ export function getSchemaForType( const program = dpgContext.program; const type = getEffectiveModelFromType(program, typeInput); - const builtinType = mapTypeSpecTypeToTypeScript(dpgContext, type, usage); + const builtinType = getSchemaForLiteral(type); if (builtinType !== undefined) { // add in description elements for types derived from primitive types (SecureString, etc.) const doc = getDoc(program, type); @@ -145,17 +147,19 @@ export function getSchemaForType( } if (type.kind === "Model") { const schema = getSchemaForModel(dpgContext, type, usage, needRef) as any; - if (usage && usage.includes(SchemaContext.Output)) { - if (!schema.name || schema.name === "") { - //TODO: HANDLE ANONYMOUS - schema.outputTypeName = - schema.type === "object" ? "Record" : "any"; - schema.typeName = - schema.type === "object" ? "Record" : "unknown"; - schema.type = "unknown"; - } else { - schema.outputTypeName = `${schema.name}Output`; - schema.typeName = `${schema.name}`; + if (!isArrayModelType(program, type) && !isRecordModelType(program, type)) { + if (usage && usage.includes(SchemaContext.Output)) { + if (!schema.name || schema.name === "") { + //TODO: HANDLE ANONYMOUS + schema.outputTypeName = + schema.type === "object" ? "Record" : "any"; + schema.typeName = + schema.type === "object" ? "Record" : "unknown"; + schema.type = "unknown"; + } else { + schema.outputTypeName = `${schema.name}Output`; + schema.typeName = `${schema.name}`; + } } } schema.usage = usage; @@ -418,6 +422,10 @@ function getSchemaForModel( usage?: SchemaContext[], needRef?: boolean ) { + if (isArrayModelType(dpgContext.program, model)) { + return getSchemaForArrayModel(dpgContext, model, usage!); + } + const program = dpgContext.program; const overridedModelName = getFriendlyName(program, model) ?? getProjectedName(program, model, "json"); @@ -443,7 +451,8 @@ function getSchemaForModel( }) .join("") + "List"; } - let modelSchema: ObjectSchema = { + + const modelSchema: ObjectSchema = { name: overridedModelName ?? name, type: "object", description: getDoc(program, model) ?? "" @@ -455,7 +464,13 @@ function getSchemaForModel( true /** shouldGuard */ ); + if (modelSchema.name === "Record" && isRecordModelType(program, model)) { + return getSchemaForRecordModel(dpgContext, model, usage!); + } modelSchema.typeName = modelSchema.name; + if (usage && usage.includes(SchemaContext.Output)) { + modelSchema.outputTypeName = modelSchema.name + "Output"; + } if (isAzureCoreErrorType(model)) { modelSchema.fromCore = true; @@ -489,6 +504,7 @@ function getSchemaForModel( } } modelSchema.properties = {}; + const derivedModels = model.derivedModels.filter(includeDerivedModel); // getSchemaOrRef on all children to push them into components.schemas @@ -545,6 +561,12 @@ function getSchemaForModel( if (needRef) { return modelSchema; } + if (isRecordModelType(program, model)) { + modelSchema.parents = { + all: [getSchemaForRecordModel(dpgContext, model, usage!)], + immediate: [getSchemaForRecordModel(dpgContext, model, usage!)] + }; + } for (const [propName, prop] of model.properties) { const restApiName = getProjectedName(program, prop, "json"); const name = `"${restApiName ?? propName}"`; @@ -617,27 +639,7 @@ function getSchemaForModel( modelSchema.properties[name] = newPropSchema; } - // Special case: if a model type extends a single *templated* base type and - // has no properties of its own, absorb the definition of the base model - // into this schema definition. The assumption here is that any model type - // defined like this is just meant to rename the underlying instance of a - // templated type. - if ( - model.baseModel && - model.baseModel.templateMapper && - model.baseModel.templateMapper.args && - model.baseModel.templateMapper.args.length > 0 && - modelSchema.properties && - Object.keys(modelSchema.properties).length === 0 - ) { - // Take the base model schema but carry across the documentation property - // that we set before - const baseSchema = getSchemaForType(dpgContext, model.baseModel, usage); - modelSchema = { - ...baseSchema, - description: modelSchema.description - }; - } else if (model.baseModel) { + if (model.baseModel) { modelSchema.parents = { all: [getSchemaForType(dpgContext, model.baseModel, usage, true)], immediate: [getSchemaForType(dpgContext, model.baseModel, usage, true)] @@ -647,11 +649,7 @@ function getSchemaForModel( } // Map an typespec type to an OA schema. Returns undefined when the resulting // OA schema is just a regular object schema. -function mapTypeSpecTypeToTypeScript( - dpgContext: SdkContext, - type: Type, - usage?: SchemaContext[] -): any { +function getSchemaForLiteral(type: Type): any { switch (type.kind) { case "Number": return { type: `${type.value}` }; @@ -659,8 +657,6 @@ function mapTypeSpecTypeToTypeScript( return { type: `"${type.value}"` }; case "Boolean": return { type: `${type.value}` }; - case "Model": - return mapTypeSpecStdTypeToTypeScript(dpgContext, type, usage); } if (type.kind === undefined) { if (typeof type === "string") { @@ -669,6 +665,7 @@ function mapTypeSpecTypeToTypeScript( return { type: `${type}` }; } } + return undefined; } function applyIntrinsicDecorators( program: Program, @@ -774,111 +771,119 @@ function enumMemberType(member: EnumMember) { /** * Map TypeSpec intrinsic models to open api definitions */ -function mapTypeSpecStdTypeToTypeScript( +function getSchemaForArrayModel( dpgContext: SdkContext, type: Model, - usage?: SchemaContext[] -): any | undefined { - const program = dpgContext.program; - const indexer = (type as Model).indexer; - if (indexer !== undefined) { - if (!isNeverType(indexer.key)) { - const name = indexer.key.name; - let schema: any = {}; - if (name === "string") { - const valueType = getSchemaForType( - dpgContext, - indexer.value!, - usage, - true - ); - schema = { - type: "dictionary", - additionalProperties: valueType, - description: getDoc(program, type) - }; - if ( - !program.checker.isStdType(indexer.value) && - !isUnknownType(indexer.value!) && - !isUnionType(indexer.value!) - ) { - schema.typeName = `Record`; - schema.valueTypeName = valueType.name; - if (usage && usage.includes(SchemaContext.Output)) { - schema.outputTypeName = `Record`; - schema.outputValueTypeName = `${valueType.outputTypeName}`; - } - } else if (isUnknownType(indexer.value!)) { - schema.typeName = `Record`; - if (usage && usage.includes(SchemaContext.Output)) { - schema.outputTypeName = `Record`; - } - } else { - schema.typeName = `Record`; - schema.outputTypeName = `Record`; - } - } else if (name === "integer") { - schema = { - type: "array", - items: getSchemaForType(dpgContext, indexer.value!, usage, true), - description: getDoc(program, type) - }; - if ( - !program.checker.isStdType(indexer.value) && - !isUnknownType(indexer.value!) && - indexer.value?.kind && - schema.items.name && - !schema.items.enum - ) { - schema.typeName = `Array<${schema.items.name}>`; - if (usage && usage.includes(SchemaContext.Output)) { - schema.outputTypeName = `Array<${schema.items.name}Output>`; - } + usage: SchemaContext[] +) { + const { program } = dpgContext; + const { indexer } = type; + let schema: any = {}; + if (!indexer) { + return schema; + } + if (isArrayModelType(program, type)) { + schema = { + type: "array", + items: getSchemaForType(dpgContext, indexer.value!, usage, true), + description: getDoc(program, type) + }; + if ( + !program.checker.isStdType(indexer.value) && + !isUnknownType(indexer.value!) && + indexer.value?.kind && + schema.items.name && + !schema.items.enum + ) { + schema.typeName = `Array<${schema.items.name}>`; + if (usage && usage.includes(SchemaContext.Output)) { + schema.outputTypeName = `Array<${schema.items.name}Output>`; + } + } else { + if (schema.items.typeName) { + if (schema.items.type === "dictionary") { + schema.typeName = `${schema.items.typeName}[]`; + } else if (schema.items.type === "union") { + schema.typeName = `(${schema.items.typeName})[]`; } else { - if (schema.items.typeName) { - if (schema.items.type === "dictionary") { - schema.typeName = `${schema.items.typeName}[]`; - } else if (schema.items.type === "union") { - schema.typeName = `(${schema.items.typeName})[]`; - } else { - schema.typeName = schema.items.typeName - .split("|") - .map((typeName: string) => { - return `${typeName}[]`; - }) - .join(" | "); - if ( - schema.items.outputTypeName && - usage && - usage.includes(SchemaContext.Output) - ) { - schema.outputTypeName = schema.items.outputTypeName - .split("|") - .map((typeName: string) => { - return `${typeName}[]`; - }) - .join(" | "); - } - } - } else if (schema.items.type.includes("|")) { - schema.typeName = `(${schema.items.type})[]`; - } else { - schema.typeName = `${schema.items.type}[]`; + schema.typeName = schema.items.typeName + .split("|") + .map((typeName: string) => { + return `${typeName}[]`; + }) + .join(" | "); + if ( + schema.items.outputTypeName && + usage && + usage.includes(SchemaContext.Output) + ) { + schema.outputTypeName = schema.items.outputTypeName + .split("|") + .map((typeName: string) => { + return `${typeName}[]`; + }) + .join(" | "); } } + } else if (schema.items.type.includes("|")) { + schema.typeName = `(${schema.items.type})[]`; + } else { + schema.typeName = `${schema.items.type}[]`; } + } + schema.usage = usage; + return schema; + } +} - schema.usage = usage; - return schema; +function getSchemaForRecordModel( + dpgContext: SdkContext, + type: Model, + usage: SchemaContext[] +) { + const { program } = dpgContext; + const { indexer } = type; + let schema: any = {}; + if (!indexer) { + return schema; + } + if (isRecordModelType(program, type)) { + const valueType = getSchemaForType(dpgContext, indexer?.value, usage, true); + schema = { + type: "dictionary", + additionalProperties: valueType, + description: getDoc(program, type) + }; + if ( + !program.checker.isStdType(indexer.value) && + !isUnknownType(indexer.value!) && + !isUnionType(indexer.value!) + ) { + schema.typeName = `Record`; + schema.valueTypeName = valueType.name; + if (usage && usage.includes(SchemaContext.Output)) { + schema.outputTypeName = `Record`; + schema.outputValueTypeName = `${valueType.outputTypeName}`; + } + } else if (isUnknownType(indexer.value!)) { + schema.typeName = `Record`; + if (usage && usage.includes(SchemaContext.Output)) { + schema.outputTypeName = `Record`; + } + } else { + schema.typeName = `Record`; + schema.outputTypeName = `Record`; } + schema.usage = usage; + return schema; } } diff --git a/packages/typespec-ts/test/modularUnit/modelsGenerator.spec.ts b/packages/typespec-ts/test/modularUnit/modelsGenerator.spec.ts index 2a1040bbb7..d8d63eab4d 100644 --- a/packages/typespec-ts/test/modularUnit/modelsGenerator.spec.ts +++ b/packages/typespec-ts/test/modularUnit/modelsGenerator.spec.ts @@ -3,7 +3,55 @@ import { emitModularModelsFromTypeSpec, emitModularOperationsFromTypeSpec } from "../util/emitUtil.js"; -import { assertEqualContent } from "../util/testUtil.js"; +import { VerifyPropertyConfig, assertEqualContent } from "../util/testUtil.js"; + +async function verifyModularPropertyType( + tspType: string, + inputType: string, + options?: VerifyPropertyConfig, + needAzureCore: boolean = false, + additionalImports: string = "" +) { + const defaultOption: VerifyPropertyConfig = { + additionalTypeSpecDefinition: "", + outputType: inputType, + additionalInputContent: "", + additionalOutputContent: "" + }; + const { + additionalTypeSpecDefinition, + additionalInputContent, + } = { + ...defaultOption, + ...options + }; + const modelsFile = await emitModularModelsFromTypeSpec( + ` + ${additionalTypeSpecDefinition} + #suppress "@azure-tools/typespec-azure-core/documentation-required" "for test" + model InputOutputModel { + prop: ${tspType}; + } + + #suppress "@azure-tools/typespec-azure-core/use-standard-operations" "for test" + #suppress "@azure-tools/typespec-azure-core/documentation-required" "for test" + @route("/models") + @get + op getModel(@body input: InputOutputModel): InputOutputModel;`, + needAzureCore + ); + assert.ok(modelsFile); + assertEqualContent( + modelsFile?.getFullText()!, + ` + ${additionalImports} + + export interface InputOutputModel { + prop: ${inputType}; + } + ${additionalInputContent}` + ); +} describe("modular model type", () => { it("shouldn't generate models if there is no operations", async () => { @@ -16,6 +64,26 @@ describe("modular model type", () => { }); }); +describe("model property type", () => { + it("should handle type_literals:boolean -> boolean_literals", async () => { + const tspType = `true`; + const typeScriptType = `true`; + await verifyModularPropertyType(tspType, typeScriptType); + }); + + it("should handle type_literals:number -> number_literals", async () => { + const tspType = `1`; + const typeScriptType = `1`; + await verifyModularPropertyType(tspType, typeScriptType); + }); + + it("should handle type_literals:string -> string_literals", async () => { + const tspType = `"foo"`; + const typeScriptType = `"foo"`; + await verifyModularPropertyType(tspType, typeScriptType); + }); +}) + describe("modular encode test for property type datetime", () => { it("should handle property type plainDate, plainTime, utcDateTime, offsetDatetime with default encoding", async () => { const tspContent = ` diff --git a/packages/typespec-ts/test/unit/modelsGenerator.spec.ts b/packages/typespec-ts/test/unit/modelsGenerator.spec.ts index be4a74765f..38f03f9381 100644 --- a/packages/typespec-ts/test/unit/modelsGenerator.spec.ts +++ b/packages/typespec-ts/test/unit/modelsGenerator.spec.ts @@ -4,14 +4,7 @@ import { emitParameterFromTypeSpec, emitResponsesFromTypeSpec } from "../util/emitUtil.js"; -import { assertEqualContent } from "../util/testUtil.js"; - -type VerifyPropertyConfig = { - additionalTypeSpecDefinition?: string; - outputType?: string; - additionalInputContent?: string; - additionalOutputContent?: string; -}; +import { VerifyPropertyConfig, assertEqualContent } from "../util/testUtil.js"; describe("Input/output model type", () => { it("shouldn't generate models if there is no operations", async () => { @@ -847,6 +840,465 @@ describe("Input/output model type", () => { }); }); }); + describe("additional properties generation", () => { + it("should handle model additional properties from record of unknown", async () => { + const schemaOutput = await emitModelsFromTypeSpec(` + model VegetableCarrot extends Record {} + model VegetableBeans extends Record {} + + model Vegetables { + carrots: VegetableCarrot, + beans: VegetableBeans + } + op post(@body body: Vegetables): { @body body: Vegetables }; + `); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + assert.ok(inputModelFile); + assert.strictEqual(inputModelFile?.path, "models.ts"); + assertEqualContent( + inputModelFile?.content!, + ` + export interface Vegetables { + carrots: VegetableCarrot; + beans: VegetableBeans; + } + + export interface VegetableCarrot extends Record {} + + export interface VegetableBeans extends Record {} + ` + ); + + assert.ok(outputModelFile); + assert.strictEqual(outputModelFile?.path, "outputModels.ts"); + assertEqualContent( + outputModelFile?.content!, + ` + export interface VegetablesOutput { + carrots: VegetableCarrotOutput; + beans: VegetableBeansOutput; + } + + export interface VegetableCarrotOutput extends Record {} + + export interface VegetableBeansOutput extends Record {} + ` + ); + }); + + it("should handle model additional properties from record of boolean", async () => { + const schemaOutput = await emitModelsFromTypeSpec(` + model VegetableCarrot extends Record {} + model VegetableBeans extends Record {} + + model Vegetables { + carrots: VegetableCarrot, + beans: VegetableBeans + } + op post(@body body: Vegetables): { @body body: Vegetables }; + `); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + assert.ok(inputModelFile); + assert.strictEqual(inputModelFile?.path, "models.ts"); + assertEqualContent( + inputModelFile?.content!, + ` + export interface Vegetables { + carrots: VegetableCarrot; + beans: VegetableBeans; + } + + export interface VegetableCarrot extends Record {} + + export interface VegetableBeans extends Record {} + ` + ); + + assert.ok(outputModelFile); + assert.strictEqual(outputModelFile?.path, "outputModels.ts"); + assertEqualContent( + outputModelFile?.content!, + ` + export interface VegetablesOutput { + carrots: VegetableCarrotOutput; + beans: VegetableBeansOutput; + } + + export interface VegetableCarrotOutput extends Record {} + + export interface VegetableBeansOutput extends Record {} + ` + ); + }); + + it("should handle model additional properties from record of float32", async () => { + const schemaOutput = await emitModelsFromTypeSpec(` + model VegetableCarrot extends Record {} + model VegetableBeans extends Record {} + + model Vegetables { + carrots: VegetableCarrot, + beans: VegetableBeans + } + op post(@body body: Vegetables): { @body body: Vegetables }; + `); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + assert.ok(inputModelFile); + assert.strictEqual(inputModelFile?.path, "models.ts"); + assertEqualContent( + inputModelFile?.content!, + ` + export interface Vegetables { + carrots: VegetableCarrot; + beans: VegetableBeans; + } + + export interface VegetableCarrot extends Record {} + + export interface VegetableBeans extends Record {} + ` + ); + + assert.ok(outputModelFile); + assert.strictEqual(outputModelFile?.path, "outputModels.ts"); + assertEqualContent( + outputModelFile?.content!, + ` + export interface VegetablesOutput { + carrots: VegetableCarrotOutput; + beans: VegetableBeansOutput; + } + + export interface VegetableCarrotOutput extends Record {} + + export interface VegetableBeansOutput extends Record {} + ` + ); + }); + + it("should handle model additional properties from record of int64", async () => { + const schemaOutput = await emitModelsFromTypeSpec(` + model VegetableCarrot extends Record {} + model VegetableBeans extends Record {} + + model Vegetables { + carrots: VegetableCarrot, + beans: VegetableBeans + } + op post(@body body: Vegetables): { @body body: Vegetables }; + `); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + assert.ok(inputModelFile); + assert.strictEqual(inputModelFile?.path, "models.ts"); + assertEqualContent( + inputModelFile?.content!, + ` + export interface Vegetables { + carrots: VegetableCarrot; + beans: VegetableBeans; + } + + export interface VegetableCarrot extends Record {} + + export interface VegetableBeans extends Record {} + ` + ); + + assert.ok(outputModelFile); + assert.strictEqual(outputModelFile?.path, "outputModels.ts"); + assertEqualContent( + outputModelFile?.content!, + ` + export interface VegetablesOutput { + carrots: VegetableCarrotOutput; + beans: VegetableBeansOutput; + } + + export interface VegetableCarrotOutput extends Record {} + + export interface VegetableBeansOutput extends Record {} + ` + ); + }); + + it("should handle model additional properties from record of string", async () => { + const schemaOutput = await emitModelsFromTypeSpec(` + model VegetableCarrot extends Record {} + model VegetableBeans extends Record {} + + model Vegetables { + carrots: VegetableCarrot, + beans: VegetableBeans + } + op post(@body body: Vegetables): { @body body: Vegetables }; + `); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + assert.ok(inputModelFile); + assert.strictEqual(inputModelFile?.path, "models.ts"); + assertEqualContent( + inputModelFile?.content!, + ` + export interface Vegetables { + carrots: VegetableCarrot; + beans: VegetableBeans; + } + + export interface VegetableCarrot extends Record {} + + export interface VegetableBeans extends Record {} + ` + ); + + assert.ok(outputModelFile); + assert.strictEqual(outputModelFile?.path, "outputModels.ts"); + assertEqualContent( + outputModelFile?.content!, + ` + export interface VegetablesOutput { + carrots: VegetableCarrotOutput; + beans: VegetableBeansOutput; + } + + export interface VegetableCarrotOutput extends Record {} + + export interface VegetableBeansOutput extends Record {} + ` + ); + }); + + it("should handle model additional properties extends from record of object", async () => { + const schemaOutput = await emitModelsFromTypeSpec(` + model VegetableCarrot extends Record { + testProp: Carrots + } + model VegetableBeans extends Record {} + + model Vegetables { + carrots: VegetableCarrot, + beans: VegetableBeans + } + model Carrots { + color: string, + id: string + } + model Beans { + expiry: string, + id: string + } + op post(@body body: Vegetables): { @body body: Vegetables }; + `); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + assert.ok(inputModelFile); + assert.strictEqual(inputModelFile?.path, "models.ts"); + assertEqualContent( + inputModelFile?.content!, + ` + export interface Vegetables { + carrots: VegetableCarrot; + beans: VegetableBeans; + } + + export interface VegetableCarrot extends Record { + testProp: Carrots; + } + + export interface Carrots { + color: string; + id: string; + } + + export interface VegetableBeans extends Record {} + + export interface Beans { + expiry: string; + id: string; + }` + ); + + assert.ok(outputModelFile); + assert.strictEqual(outputModelFile?.path, "outputModels.ts"); + assertEqualContent( + outputModelFile?.content!, + ` + export interface VegetablesOutput { + carrots: VegetableCarrotOutput; + beans: VegetableBeansOutput; + } + + export interface VegetableCarrotOutput extends Record { + testProp: CarrotsOutput; + } + + export interface CarrotsOutput { + color: string; + id: string; + } + + export interface VegetableBeansOutput extends Record {} + + export interface BeansOutput { + expiry: string; + id: string; + }` + ); + }); + + it("should handle model additional properties is from record of object", async () => { + const schemaOutput = await emitModelsFromTypeSpec(` + model VegetableCarrot is Record { + testProp: Carrots + } + model VegetableBeans is Record {} + + model Vegetables { + carrots: VegetableCarrot, + beans: VegetableBeans + } + model Carrots { + color: string, + id: string + } + model Beans { + expiry: string, + id: string + } + op post(@body body: Vegetables): { @body body: Vegetables }; + `); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + assert.ok(inputModelFile); + assert.strictEqual(inputModelFile?.path, "models.ts"); + assertEqualContent( + inputModelFile?.content!, + ` + export interface Vegetables { + carrots: VegetableCarrot; + beans: VegetableBeans; + } + + export interface VegetableCarrot extends Record { + testProp: Carrots; + } + + export interface Carrots { + color: string; + id: string; + } + + export interface VegetableBeans extends Record {} + + export interface Beans { + expiry: string; + id: string; + }` + ); + + assert.ok(outputModelFile); + assert.strictEqual(outputModelFile?.path, "outputModels.ts"); + assertEqualContent( + outputModelFile?.content!, + ` + export interface VegetablesOutput { + carrots: VegetableCarrotOutput; + beans: VegetableBeansOutput; + } + + export interface VegetableCarrotOutput extends Record { + testProp: CarrotsOutput; + } + + export interface CarrotsOutput { + color: string; + id: string; + } + + export interface VegetableBeansOutput extends Record {} + + export interface BeansOutput { + expiry: string; + id: string; + }` + ); + }); + + it("should handle model additional properties from record of object array", async () => { + const schemaOutput = await emitModelsFromTypeSpec(` + model VegetableCarrot extends Record {} + model VegetableBeans extends Record {} + + model Vegetables { + carrots: VegetableCarrot, + beans: VegetableBeans + } + model Carrots { + color: string, + id: string + } + model Beans { + expiry: string, + id: string + } + op read(@body body: Vegetables): { @body body: Vegetables }; + `); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + assert.ok(inputModelFile); + assert.strictEqual(inputModelFile?.path, "models.ts"); + assertEqualContent( + inputModelFile?.content!, + ` + export interface Vegetables { + carrots: VegetableCarrot; + beans: VegetableBeans; + } + + export interface VegetableCarrot extends Record> {} + + export interface Carrots { + color: string; + id: string; + } + + export interface VegetableBeans extends Record> {} + + export interface Beans { + expiry: string; + id: string; + }` + ); + + assert.ok(outputModelFile); + assert.strictEqual(outputModelFile?.path, "outputModels.ts"); + assertEqualContent( + outputModelFile?.content!, + ` + export interface VegetablesOutput { + carrots: VegetableCarrotOutput; + beans: VegetableBeansOutput; + } + + export interface VegetableCarrotOutput extends Record> {} + + export interface CarrotsOutput { + color: string; + id: string; + } + + export interface VegetableBeansOutput extends Record> {} + + export interface BeansOutput { + expiry: string; + id: string; + }` + ); + }); + }) describe("bytes generation as property", () => { it("should handle bytes -> string", async () => { await verifyPropertyType("bytes", "string"); diff --git a/packages/typespec-ts/test/util/testUtil.ts b/packages/typespec-ts/test/util/testUtil.ts index 0a060d59a3..be23928445 100644 --- a/packages/typespec-ts/test/util/testUtil.ts +++ b/packages/typespec-ts/test/util/testUtil.ts @@ -105,3 +105,10 @@ export function assertEqualContent( ) ); } + +export type VerifyPropertyConfig = { + additionalTypeSpecDefinition?: string; + outputType?: string; + additionalInputContent?: string; + additionalOutputContent?: string; +}; \ No newline at end of file