Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/openapi3"
---

importer - OpenAPI number type with duration format now converts to TypeSpec duration type with @encode("seconds", float32) decorator
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,13 @@ export function getTypeSpecPrimitiveFromSchema(schema: SupportedOpenAPISchema):

function getIntegerType(schema: SupportedOpenAPISchema): string {
const format = schema.format ?? "";

// Check for x-ms-duration extension
const xmsDuration = (schema as any)["x-ms-duration"];
if (xmsDuration === "seconds" || xmsDuration === "milliseconds") {
return "duration";
}

switch (format) {
case "int8":
case "int16":
Expand All @@ -556,6 +563,13 @@ function getIntegerType(schema: SupportedOpenAPISchema): string {

function getNumberType(schema: SupportedOpenAPISchema): string {
const format = schema.format ?? "";

// Check for x-ms-duration extension
const xmsDuration = (schema as any)["x-ms-duration"];
if (xmsDuration === "seconds" || xmsDuration === "milliseconds") {
return "duration";
}

switch (format) {
case "decimal":
case "decimal128":
Expand Down
65 changes: 65 additions & 0 deletions packages/openapi3/src/cli/actions/convert/utils/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ export function getDecoratorsForSchema(
? schema.type.find((t) => t !== "null")
: schema.type;

// Handle x-ms-duration extension with @encode decorator
// Must be after effectiveType extraction to handle type arrays correctly
const xmsDuration = (schema as any)["x-ms-duration"];
if (xmsDuration === "seconds" || xmsDuration === "milliseconds") {
decorators.push(...getDurationSchemaDecorators(schema, effectiveType));
}

// Handle unixtime format with @encode decorator
// Check both direct format and format from anyOf/oneOf members
let formatToUse = schema.format;
Expand Down Expand Up @@ -322,6 +329,64 @@ function getUnixtimeSchemaDecorators(effectiveType: string | undefined) {
return decorators;
}

function getDurationSchemaDecorators(
schema: OpenAPI3Schema | OpenAPISchema3_1,
effectiveType: string | undefined,
) {
const decorators: TypeSpecDecorator[] = [];

// Get the x-ms-duration value (seconds or milliseconds)
const xmsDuration = (schema as any)["x-ms-duration"];
if (!xmsDuration || (xmsDuration !== "seconds" && xmsDuration !== "milliseconds")) {
return decorators;
}

// Determine the encoding type based on the schema's format and type
let encodingType = "float32"; // default
const format = schema.format ?? "";

if (effectiveType === "integer") {
// For integer types, use the specific format or default to integer
switch (format) {
case "int8":
case "int16":
case "int32":
case "int64":
case "uint8":
case "uint16":
case "uint32":
case "uint64":
encodingType = format;
break;
default:
encodingType = "integer";
}
} else if (effectiveType === "number") {
// For number types, use the specific format or default to float32
switch (format) {
case "decimal":
case "decimal128":
encodingType = format;
break;
case "double":
encodingType = "float64";
break;
case "float":
encodingType = "float32";
break;
default:
encodingType = "float32";
}
}

decorators.push({
name: "encode",
args: [createTSValue(`"${xmsDuration}"`), createTSValue(encodingType)],
});

return decorators;
}

const knownStringFormats = new Set([
"binary",
"byte",
Expand Down
192 changes: 192 additions & 0 deletions packages/openapi3/test/tsp-openapi3/data-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,198 @@ describe("converts top-level schemas", () => {
]);
});

describe("handles duration types with x-ms-duration extension", () => {
it("string with duration format -> duration (no encoding)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
DurationString: {
type: "string",
format: "duration",
},
},
});

const scalars = serviceNamespace.scalars;
/* scalar DurationString extends duration; */
expect(scalars.get("DurationString")?.baseScalar?.name).toBe("duration");
expect(scalars.get("DurationString")!.decorators.length).toBe(0);
});

it("integer with int32 format and no extension -> int32 (no encoding)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
IntegerInt32: {
type: "integer",
format: "int32",
},
},
});

const scalars = serviceNamespace.scalars;
/* scalar IntegerInt32 extends int32; */
expect(scalars.get("IntegerInt32")?.baseScalar?.name).toBe("int32");
expect(scalars.get("IntegerInt32")!.decorators.length).toBe(0);
});

it("integer with int32 format and x-ms-duration: seconds -> duration with @encode(seconds, int32)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
DurationIntSeconds: {
type: "integer",
format: "int32",
"x-ms-duration": "seconds",
} as any,
},
});

const scalars = serviceNamespace.scalars;
/* @extension("x-ms-duration", "seconds") @encode("seconds", int32) scalar DurationIntSeconds extends duration; */
expect(scalars.get("DurationIntSeconds")?.baseScalar?.name).toBe("duration");
// Note: TypeSpec compiler may reorder decorators during parsing
expectDecorators(scalars.get("DurationIntSeconds")!.decorators, [
{ name: "encode", args: ["seconds", { kind: "Scalar", name: "int32" }] },
{ name: "extension", args: ["x-ms-duration", "seconds"] },
]);
});

it("integer with int32 format and x-ms-duration: milliseconds -> duration with @encode(milliseconds, int32)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
DurationIntMilliseconds: {
type: "integer",
format: "int32",
"x-ms-duration": "milliseconds",
} as any,
},
});

const scalars = serviceNamespace.scalars;
/* @extension("x-ms-duration", "milliseconds") @encode("milliseconds", int32) scalar DurationIntMilliseconds extends duration; */
expect(scalars.get("DurationIntMilliseconds")?.baseScalar?.name).toBe("duration");
// Note: TypeSpec compiler may reorder decorators during parsing
expectDecorators(scalars.get("DurationIntMilliseconds")!.decorators, [
{ name: "encode", args: ["milliseconds", { kind: "Scalar", name: "int32" }] },
{ name: "extension", args: ["x-ms-duration", "milliseconds"] },
]);
});

it("number with float format and no extension -> float32 (no encoding)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
NumberFloat: {
type: "number",
format: "float",
},
},
});

const scalars = serviceNamespace.scalars;
/* scalar NumberFloat extends float32; */
expect(scalars.get("NumberFloat")?.baseScalar?.name).toBe("float32");
expect(scalars.get("NumberFloat")!.decorators.length).toBe(0);
});

it("number with float format and x-ms-duration: seconds -> duration with @encode(seconds, float32)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
DurationFloatSeconds: {
type: "number",
format: "float",
"x-ms-duration": "seconds",
} as any,
},
});

const scalars = serviceNamespace.scalars;
/* @extension("x-ms-duration", "seconds") @encode("seconds", float32) scalar DurationFloatSeconds extends duration; */
expect(scalars.get("DurationFloatSeconds")?.baseScalar?.name).toBe("duration");
// Note: TypeSpec compiler may reorder decorators during parsing
expectDecorators(scalars.get("DurationFloatSeconds")!.decorators, [
{ name: "encode", args: ["seconds", { kind: "Scalar", name: "float32" }] },
{ name: "extension", args: ["x-ms-duration", "seconds"] },
]);
});

it("number with float format and x-ms-duration: milliseconds -> duration with @encode(milliseconds, float32)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
DurationFloatMilliseconds: {
type: "number",
format: "float",
"x-ms-duration": "milliseconds",
} as any,
},
});

const scalars = serviceNamespace.scalars;
/* @extension("x-ms-duration", "milliseconds") @encode("milliseconds", float32) scalar DurationFloatMilliseconds extends duration; */
expect(scalars.get("DurationFloatMilliseconds")?.baseScalar?.name).toBe("duration");
// Note: TypeSpec compiler may reorder decorators during parsing
expectDecorators(scalars.get("DurationFloatMilliseconds")!.decorators, [
{ name: "encode", args: ["milliseconds", { kind: "Scalar", name: "float32" }] },
{ name: "extension", args: ["x-ms-duration", "milliseconds"] },
]);
});

it("number with decimal format and no extension -> decimal (no encoding)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
NumberDecimal: {
type: "number",
format: "decimal",
},
},
});

const scalars = serviceNamespace.scalars;
/* scalar NumberDecimal extends decimal; */
expect(scalars.get("NumberDecimal")?.baseScalar?.name).toBe("decimal");
expect(scalars.get("NumberDecimal")!.decorators.length).toBe(0);
});

it("number with decimal format and x-ms-duration: seconds -> duration with @encode(seconds, decimal)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
DurationDecimalSeconds: {
type: "number",
format: "decimal",
"x-ms-duration": "seconds",
} as any,
},
});

const scalars = serviceNamespace.scalars;
/* @extension("x-ms-duration", "seconds") @encode("seconds", decimal) scalar DurationDecimalSeconds extends duration; */
expect(scalars.get("DurationDecimalSeconds")?.baseScalar?.name).toBe("duration");
// Note: TypeSpec compiler may reorder decorators during parsing
expectDecorators(scalars.get("DurationDecimalSeconds")!.decorators, [
{ name: "encode", args: ["seconds", { kind: "Scalar", name: "decimal" }] },
{ name: "extension", args: ["x-ms-duration", "seconds"] },
]);
});

it("number with decimal format and x-ms-duration: milliseconds -> duration with @encode(milliseconds, decimal)", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
DurationDecimalMilliseconds: {
type: "number",
format: "decimal",
"x-ms-duration": "milliseconds",
} as any,
},
});

const scalars = serviceNamespace.scalars;
/* @extension("x-ms-duration", "milliseconds") @encode("milliseconds", decimal) scalar DurationDecimalMilliseconds extends duration; */
expect(scalars.get("DurationDecimalMilliseconds")?.baseScalar?.name).toBe("duration");
// Note: TypeSpec compiler may reorder decorators during parsing
expectDecorators(scalars.get("DurationDecimalMilliseconds")!.decorators, [
{ name: "encode", args: ["milliseconds", { kind: "Scalar", name: "decimal" }] },
{ name: "extension", args: ["x-ms-duration", "milliseconds"] },
]);
});
});

it("handles arrays", async () => {
const serviceNamespace = await tspForOpenAPI3({
schemas: {
Expand Down
17 changes: 17 additions & 0 deletions packages/openapi3/test/tsp-openapi3/generate-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ const testScenarios: TestScenario[] = [
{ schema: { type: "number", format: "double" }, expected: "float64" },
{ schema: { type: "number", format: "float" }, expected: "float32" },
{ schema: { type: "number", enum: [3.14, 6.28, 42] }, expected: "3.14 | 6.28 | 42" },
// Duration tests with x-ms-duration extension
{
schema: { type: "integer", format: "int32", "x-ms-duration": "seconds" } as any,
expected: "duration",
},
{
schema: { type: "integer", format: "int32", "x-ms-duration": "milliseconds" } as any,
expected: "duration",
},
{
schema: { type: "number", format: "float", "x-ms-duration": "seconds" } as any,
expected: "duration",
},
{
schema: { type: "number", format: "float", "x-ms-duration": "milliseconds" } as any,
expected: "duration",
},
// strings
{ schema: { type: "string" }, expected: "string" },
{
Expand Down