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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/openapi3"
---

fixed a bug for SSE import where imports would be missing for other operations than get
Original file line number Diff line number Diff line change
Expand Up @@ -221,25 +221,22 @@ function generateResponseExpressions({

return contents.map(([mediaType, content]) => {
// Special handling for Server-Sent Events
if (mediaType === "text/event-stream") {
context.markSSEUsage();

if (
!context.openApi3Doc.openapi.startsWith("3.0") &&
!context.openApi3Doc.openapi.startsWith("3.1") &&
mediaType === "text/event-stream" &&
"itemSchema" in content &&
content.itemSchema &&
typeof content.itemSchema === "object" &&
"$ref" in content.itemSchema
) {
// Check for itemSchema (OpenAPI 3.2 extension)
if ("itemSchema" in content && content.itemSchema) {
const itemSchema = content.itemSchema;
if (itemSchema && typeof itemSchema === "object" && "$ref" in itemSchema) {
const eventUnionType = context.generateTypeFromRefableSchema(itemSchema, operationScope);
return `SSEStream<${eventUnionType}>`;
}
} else if (content.schema && "$ref" in content.schema) {
// Fallback: use schema directly if no itemSchema
const eventUnionType = context.generateTypeFromRefableSchema(
content.schema,
operationScope,
);
return `SSEStream<${eventUnionType}>`;
}

const eventUnionType = context.generateTypeFromRefableSchema(
content.itemSchema,
operationScope,
);
context.markSSEUsage();
return `SSEStream<${eventUnionType}>`;
// If no proper schema reference, fall through to regular handling
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ export function transform(context: Context): TypeSpecProgram {
scanForMultipartSchemas(openapi, context);

// Pre-scan for SSE event schemas before generating models
scanForSSESchemas(openapi, context);
if (
!context.openApi3Doc.openapi.startsWith("3.0") &&
!context.openApi3Doc.openapi.startsWith("3.1")
) {
scanForSSESchemas(openapi, context);
}

const models = collectDataTypes(context);
const operations = transformPaths(openapi.paths, context);
Expand Down Expand Up @@ -81,13 +86,12 @@ function scanForSSESchemas(openapi: SupportedOpenAPIDocuments, context: Context)
scanPathForSSESchemas(path, context);
}
}
const methods = ["get", "post", "put", "patch", "delete", "head"] as const;

function scanPathForMultipartSchemas(
path: OpenAPI3PathItem | OpenAPIPathItem3_2,
context: Context,
): void {
const methods = ["get", "post", "put", "patch", "delete", "head"] as const;

for (const method of methods) {
const operation = path[method];
if (!operation?.requestBody) continue;
Expand Down Expand Up @@ -131,16 +135,35 @@ function scanPathForSSESchemas(
path: OpenAPI3PathItem | OpenAPIPathItem3_2,
context: Context,
): void {
if (!("get" in path && path.get && path.get.responses)) {
// even though text/event-stream can be defined with other methods, it's only usable with GET because of the WHATWG specification
return;
for (const method of methods) {
const operation = path[method];
if (!operation?.responses) continue;

// Handle responses which could be a reference or actual responses
const responses = resolveReference(operation.responses, context);
if (!responses) return;

scanOperationForSSESchemas(responses, context);
}
if ("query" in path && path.query && path.query.responses) {
// Handle responses which could be a reference or actual responses
const responses = resolveReference(path.query.responses, context);
if (!responses) return;

// Handle responses which could be a reference or actual responses
const responses = resolveReference(path.get.responses, context);
if (!responses) return;
scanOperationForSSESchemas(responses, context);
}

scanOperationForSSESchemas(responses, context);
if ("additionalOperations" in path && path.additionalOperations) {
for (const additionalOperation of Object.values(path.additionalOperations)) {
if (additionalOperation.responses) {
// Handle responses which could be a reference or actual responses
const responses = resolveReference(additionalOperation.responses, context);
if (!responses) return;

scanOperationForSSESchemas(responses, context);
}
}
}
}

function scanOperationForSSESchemas(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import "@typespec/http";
import "@typespec/openapi";
import "@typespec/openapi3";

using Http;
using OpenAPI;

@service(#{ title: "SSE Import Scenarios" })
@info(#{ version: "0.0.0" })
namespace SSEImportScenarios;

model UserConnect {
username: string;
}

model UserMessage {
text: string;
}

@oneOf
union ChannelEventsNoTerminal {
{
event?: "userconnect",
data?: unknown,
},
{
event?: "usermessage",
data?: unknown,
},
}

@oneOf
union ChannelEventsWithTerminal {
{
data?: "[done]",
},
{
event?: "userconnect",
data?: unknown,
},
{
event?: "usermessage",
data?: unknown,
},
}

@oneOf
union ChannelEventWithCustomContentType {
{
event?: "binary",
data?: unknown,
},
}

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

@route("/channel/no-terminal-post") @post op subscribeToChannelNoTerminal(
/** Request body for subscribing to a channel without terminal event. */
@body body: {
channelId?: string;
},
): {
@header contentType: "text/event-stream";
};

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

@route("/data/custom-content-type") @get op subscribeToDataStream(): {
@header contentType: "text/event-stream";
};
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ union ChannelEventWithCustomContentType {
@route("/channel/no-terminal") @get op subscribeToChannelNoTerminal(
): SSEStream<ChannelEventsNoTerminal>;

@route("/channel/no-terminal-post") @post op subscribeToChannelNoTerminal(
/** Request body for subscribing to a channel without terminal event. */
@body body: {
channelId?: string;
},
): SSEStream<ChannelEventsNoTerminal>;

@route("/channel/with-terminal") @get op subscribeToChannelWithTerminal(
): SSEStream<ChannelEventsWithTerminal>;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
openapi: 3.1.0
info:
title: SSE Import Scenarios
version: 0.0.0
components:
schemas:
UserConnect:
type: object
required: [username]
properties:
username:
type: string
UserMessage:
type: object
required: [text]
properties:
text:
type: string
ChannelEventsNoTerminal:
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"
ChannelEventsWithTerminal:
type: object
properties:
event:
type: string
data:
type: string
required: [event]
# Define event types and specific schemas for the corresponding data
oneOf:
- properties:
data:
contentMediaType: text/plain
const: "[done]"
x-ms-sse-terminal-event: true
- 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"
ChannelEventWithCustomContentType:
type: object
properties:
event:
type: string
data:
type: string
required: [event]
oneOf:
- properties:
event:
const: binary
data:
contentMediaType: application/octet-stream
contentSchema:
type: object
required: [data]
properties:
data:
type: string
format: byte
paths:
/channel/no-terminal:
get:
operationId: subscribeToChannelNoTerminal
responses:
"200":
description: A request body to add a stream of typed data.
content:
text/event-stream:
itemSchema:
$ref: "#/components/schemas/ChannelEventsNoTerminal"
/channel/no-terminal-post:
post:
operationId: subscribeToChannelNoTerminal
requestBody:
description: Request body for subscribing to a channel without terminal event.
required: true
content:
application/json:
schema:
type: object
properties:
channelId:
type: string
responses:
"200":
description: A request body to add a stream of typed data.
content:
text/event-stream:
itemSchema:
$ref: "#/components/schemas/ChannelEventsNoTerminal"
/channel/with-terminal:
get:
operationId: subscribeToChannelWithTerminal
responses:
"200":
description: A request body to add a stream of typed data.
content:
text/event-stream:
itemSchema:
$ref: "#/components/schemas/ChannelEventsWithTerminal"
/data/custom-content-type:
get:
operationId: subscribeToDataStream
responses:
"200":
description: A data stream with custom content types.
content:
text/event-stream:
itemSchema:
$ref: "#/components/schemas/ChannelEventWithCustomContentType"
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ paths:
text/event-stream:
itemSchema:
$ref: "#/components/schemas/ChannelEventsNoTerminal"
/channel/no-terminal-post:
post:
operationId: subscribeToChannelNoTerminal
requestBody:
description: Request body for subscribing to a channel without terminal event.
required: true
content:
application/json:
schema:
type: object
properties:
channelId:
type: string
responses:
"200":
description: A request body to add a stream of typed data.
content:
text/event-stream:
itemSchema:
$ref: "#/components/schemas/ChannelEventsNoTerminal"
/channel/with-terminal:
get:
operationId: subscribeToChannelWithTerminal
Expand Down
Loading