Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

support additional properties in RLC #2054

Merged
merged 12 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/rlc-common/src/buildIndexFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ function generateRLCIndex(file: SourceFile, model: RLCModel) {
hasMultiCollection(model) ||
hasSsvCollection(model) ||
hasPipeCollection(model) ||
hasTsvCollection(model) ||
hasTsvCollection(model) ||
hasCsvCollection(model)
) {
file.addExportDeclarations([
Expand Down
26 changes: 18 additions & 8 deletions packages/rlc-common/src/buildObjectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,24 +363,34 @@ 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<string, unknown> to the extend list and
if (objectSchema.parents.immediate.find(isDictionarySchema)) {
if (
MaryGao marked this conversation as resolved.
Show resolved Hide resolved
objectSchema.parents.immediate.find((im) => isDictionarySchema(im, true))
) {
extendFrom.push("Record<string, unknown>");
}

// Get the rest of the parents excluding any DictionarySchemas
const parents = objectSchema.parents.immediate
.filter((p) => !isDictionarySchema(p))
.filter((p) => !isDictionarySchema(p, true))
.map((parent) => {
const nameSuffix = schemaUsage.includes(SchemaContext.Output)
? "Output"
: "";
const name = `${normalizeName(
parent.name,
NameType.Interface,
true /** shouldGuard */
)}${nameSuffix}`;
const name = isDictionarySchema(parent)
? `${normalizeName(
(schemaUsage.includes(SchemaContext.Output)
? parent.outputTypeName
: parent.typeName) ?? parent.name,
NameType.Interface,
true /** shouldGuard */
)}`
: `${normalizeName(
parent.name,
NameType.Interface,
true /** shouldGuard */
)}${nameSuffix}`;

return isObjectSchema(parent) && isPolymorphicParent(parent)
? `${name}Parent`
Expand Down
9 changes: 7 additions & 2 deletions packages/rlc-common/src/helpers/schemaHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@

import { Schema } from "../interfaces.js";

export function isDictionarySchema(schema: Schema) {
export function isDictionarySchema(
schema: Schema,
filterEmpty: boolean = false
qiaozha marked this conversation as resolved.
Show resolved Hide resolved
qiaozha marked this conversation as resolved.
Show resolved Hide resolved
) {
if (schema.type === "dictionary") {
return true;
if (!filterEmpty || (filterEmpty && !schema.typeName)) {
return true;
}
}
return false;
}
Expand Down
6 changes: 5 additions & 1 deletion packages/typespec-ts/src/modular/emitModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-ts/src/modular/helpers/typeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
qiaozha marked this conversation as resolved.
Show resolved Hide resolved
if (type.valueType?.type === "string") {
typeName = type.value ? `"${type.value}"` : "undefined";
}
Expand Down
24 changes: 2 additions & 22 deletions packages/typespec-ts/src/utils/modelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ function getSchemaForModel(
})
.join("") + "List";
}
let modelSchema: ObjectSchema = {
const modelSchema: ObjectSchema = {
name: overridedModelName ?? name,
type: "object",
description: getDoc(program, model) ?? ""
Expand Down Expand Up @@ -617,27 +617,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) {
qiaozha marked this conversation as resolved.
Show resolved Hide resolved
modelSchema.parents = {
all: [getSchemaForType(dpgContext, model.baseModel, usage, true)],
immediate: [getSchemaForType(dpgContext, model.baseModel, usage, true)]
Expand Down
70 changes: 69 additions & 1 deletion packages/typespec-ts/test/modularUnit/modelsGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 = `
Expand Down
56 changes: 48 additions & 8 deletions packages/typespec-ts/test/unit/modelsGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -845,6 +838,53 @@ describe("Input/output model type", () => {
});
});
});

it("should handle model extends from record", async () => {
const schemaOutput = await emitModelsFromTypeSpec(`
model VegetableCarrot extends Record<Carrots[]> {}
model VegetableBeans extends Record<Beans[]> {}

model Vegetables {
carrots: VegetableCarrot,
beans: VegetableBeans
}
model Carrots {
color: string,
id: string
}
model Beans {
expiry: string,
id: string
}
op read(): { @body body: Vegetables };
`);
assert.ok(schemaOutput);
const { inputModelFile, outputModelFile } = schemaOutput!;
assert.ok(!inputModelFile?.content);
assert.strictEqual(outputModelFile?.path, "outputModels.ts");
assertEqualContent(
outputModelFile?.content!,
`
export interface VegetablesOutput {
carrots: VegetableCarrotOutput;
beans: VegetableBeansOutput;
}

export interface VegetableCarrotOutput extends Record<string, Array<CarrotsOutput>> {}

export interface CarrotsOutput {
color: string;
id: string;
}

export interface VegetableBeansOutput extends Record<string, Array<BeansOutput>> {}

export interface BeansOutput {
expiry: string;
id: string;
}`
);
});
});
});
describe("bytes generation as property", () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/typespec-ts/test/util/testUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,10 @@ export function assertEqualContent(
)
);
}

export type VerifyPropertyConfig = {
additionalTypeSpecDefinition?: string;
outputType?: string;
additionalInputContent?: string;
additionalOutputContent?: string;
};
Loading