Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions .chronus/changes/fix-events-import-2025-11-23-10-21-25.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/openapi3"
---

import tool - missing imports for SSE events
7 changes: 7 additions & 0 deletions .chronus/changes/fix-events-import-2025-11-24-9-51-57.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/openapi3"
---

import tool - escape SSE event union identifiers when required
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function generateMain(program: TypeSpecProgram, context: Context): string
import "@typespec/events";`
: "";

const sseUsings = context.isSSEUsed() ? "\n using SSE;" : "";
const sseUsings = context.isSSEUsed() ? "\n using SSE;\n using Events;" : "";

return `
import "@typespec/http";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ function generateSSEEventVariants(
decorators += `\n @extension("${SSE_TERMINAL_EVENT_EXTENSION}", true)`;
}

return `${decorators}\n ${eventName}: ${dataType},`;
return `${decorators}\n ${printIdentifier(eventName)}: ${dataType},`;
} catch (error) {
// If any error occurs, fall back to regular generation
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function transformComponentSchemas(context: Context, models: TypeSpecData
if (context.isSSEEventSchema(schemaRef)) {
// Remove @oneOf decorator if present and add @events
decorators = decorators.filter((d) => d.name !== "oneOf");
decorators.push({ name: "events", args: [] });
decorators.push({ name: "TypeSpec.Events.events", args: [] });
}

const union: TypeSpecUnion = {
Expand Down
39 changes: 39 additions & 0 deletions packages/openapi3/test/sse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,45 @@ describe("openapi3: SSE (Server-Sent Events)", () => {
deepStrictEqual(terminalVariant.properties.data.contentMediaType, "text/plain");
deepStrictEqual(terminalVariant["x-ms-sse-terminal-event"], true);
});

it("emits itemSchema with terminal event marked by extension with a discriminated member", async () => {
const openApi = await openApiFor(
`
model UserConnect {
username: string;
}

model UserMessage {
text: string;
}

@events
union ChannelEvents {
userconnect: UserConnect,
@terminalEvent
@extension("x-ms-sse-terminal-event", true)
usermessage: UserMessage,
}

@route("/channel")
@get op subscribe(): SSEStream<ChannelEvents>;
`,
);

ok(openApi.paths["/channel"], "expected /channel path");
const response = openApi.paths["/channel"].get.responses["200"];
const eventStreamContent = response.content["text/event-stream"];
ok(eventStreamContent.itemSchema, "expected itemSchema for SSE");

// Check oneOf includes all three events
ok(eventStreamContent.itemSchema.oneOf, "expected oneOf");
deepStrictEqual(eventStreamContent.itemSchema.oneOf.length, 2);

// Check terminal event
const terminalVariant = eventStreamContent.itemSchema.oneOf[1];
ok(terminalVariant, "expected terminal event variant");
deepStrictEqual(terminalVariant["x-ms-sse-terminal-event"], true);
});
});

describe("SSE stream with custom content types", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ union ChannelEventWithCustomContentType {
},
}

@route("/channel/no-terminal") @get op subscribeToChannelNoTerminal(): {
@route("/channel/no-terminal") @get op subscribeToChannelNoTerminalGet(): {
@header contentType: "text/event-stream";
};

@route("/channel/no-terminal-post") @post op subscribeToChannelNoTerminal(
@route("/channel/no-terminal-post") @post op subscribeToChannelNoTerminalPost(
/** Request body for subscribing to a channel without terminal event. */
@body body: {
channelId?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "@typespec/events";
using Http;
using OpenAPI;
using SSE;
using Events;

@service(#{ title: "SSE Import Scenarios" })
@info(#{ version: "0.0.0" })
Expand All @@ -21,13 +22,13 @@ model UserMessage {
text: string;
}

@events
@TypeSpec.Events.events
union ChannelEventsNoTerminal {
userconnect: UserConnect,
usermessage: UserMessage,
}

@events
@TypeSpec.Events.events
union ChannelEventsWithTerminal {
userconnect: UserConnect,
usermessage: UserMessage,
Expand All @@ -38,16 +39,25 @@ union ChannelEventsWithTerminal {
"[done]",
}

@events
@TypeSpec.Events.events
union ChannelEventsWithDiscriminatedTerminal {
userconnect: UserConnect,

@TypeSpec.SSE.terminalEvent
@extension("x-ms-sse-terminal-event", true)
usermessage: UserMessage,
}

@TypeSpec.Events.events
union ChannelEventWithCustomContentType {
@TypeSpec.Events.contentType("application/octet-stream")
binary: Base64,
}

@route("/channel/no-terminal") @get op subscribeToChannelNoTerminal(
@route("/channel/no-terminal") @get op subscribeToChannelNoTerminalGet(
): SSEStream<ChannelEventsNoTerminal>;

@route("/channel/no-terminal-post") @post op subscribeToChannelNoTerminal(
@route("/channel/no-terminal-post") @post op subscribeToChannelNoTerminalPost(
/** Request body for subscribing to a channel without terminal event. */
@body body: {
channelId?: string;
Expand All @@ -57,5 +67,8 @@ union ChannelEventWithCustomContentType {
@route("/channel/with-terminal") @get op subscribeToChannelWithTerminal(
): SSEStream<ChannelEventsWithTerminal>;

@route("/channel/with-discriminated-terminal") @get op subscribeToChannelWithDiscriminatedTerminal(
): SSEStream<ChannelEventsWithDiscriminatedTerminal>;

@route("/data/custom-content-type") @get op subscribeToDataStream(
): SSEStream<ChannelEventWithCustomContentType>;
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ components:
paths:
/channel/no-terminal:
get:
operationId: subscribeToChannelNoTerminal
operationId: subscribeToChannelNoTerminalGet
responses:
"200":
description: A request body to add a stream of typed data.
Expand All @@ -103,7 +103,7 @@ paths:
$ref: "#/components/schemas/ChannelEventsNoTerminal"
/channel/no-terminal-post:
post:
operationId: subscribeToChannelNoTerminal
operationId: subscribeToChannelNoTerminalPost
requestBody:
description: Request body for subscribing to a channel without terminal event.
required: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@ components:
contentMediaType: application/json
contentSchema:
$ref: "#/components/schemas/UserMessage"
ChannelEventsWithDiscriminatedTerminal:
type: object
properties:
event:
type: string
data:
type: string
required: [event]
# Define event types and specific schemas for the corresponding data
oneOf:
- properties:
event:
const: userconnect
data:
contentMediaType: application/json
contentSchema:
$ref: "#/components/schemas/UserConnect"
- properties:
event:
const: usermessage
data:
contentMediaType: application/json
contentSchema:
$ref: "#/components/schemas/UserMessage"
x-ms-sse-terminal-event: true
ChannelEventWithCustomContentType:
type: object
properties:
Expand All @@ -93,7 +118,7 @@ components:
paths:
/channel/no-terminal:
get:
operationId: subscribeToChannelNoTerminal
operationId: subscribeToChannelNoTerminalGet
responses:
"200":
description: A request body to add a stream of typed data.
Expand All @@ -103,7 +128,7 @@ paths:
$ref: "#/components/schemas/ChannelEventsNoTerminal"
/channel/no-terminal-post:
post:
operationId: subscribeToChannelNoTerminal
operationId: subscribeToChannelNoTerminalPost
requestBody:
description: Request body for subscribing to a channel without terminal event.
required: true
Expand Down Expand Up @@ -131,6 +156,16 @@ paths:
text/event-stream:
itemSchema:
$ref: "#/components/schemas/ChannelEventsWithTerminal"
/channel/with-discriminated-terminal:
get:
operationId: subscribeToChannelWithDiscriminatedTerminal
responses:
"200":
description: A request body to add a stream of typed data.
content:
text/event-stream:
itemSchema:
$ref: "#/components/schemas/ChannelEventsWithDiscriminatedTerminal"
/data/custom-content-type:
get:
operationId: subscribeToDataStream
Expand Down
Loading