Skip to content

[Feat] OpenAPI - add support for SSE import and emission #8887

@baywet

Description

@baywet

Clear and concise description of the problem

Now that we have support for OpenAPI 3.2.0 (#8828 ) both in emissions and in importing descriptions to TypeSpec, it'd be nice to have support for importing SSE (server-sent events).
@timotheeguerin and I spent the better part of an hour combing through multiple specifications to understand what needs to happen here, so strap in, this is going to be a long issue.

Relevant resources

Import

Scenario 1 : no terminal event

Given the following OpenAPI Response object.

content:
  description: A request body to add a stream of typed data.
  required: true
  content:
    text/event-stream:
      itemSchema:
        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:
                  type: object
                  required: [username]
                  properties:
                    username:
                      type: string
          - properties:
              event:
                const: usermessage
              data:
                contentMediaType: application/json
                contentSchema:
                  type: object
                  required: [text]
                  properties:
                    text:
                      type: string

I'd expect aresulting TypeSpec description looking like this.

import "@typespec/streams";
import "@typespec/sse";
import "@typespec/events";

using SSE;

model UserConnect {
  username: string;
}
model UserMessage {
  text: string;
}

@TypeSpec.Events.events
union ChannelEvents {
  userconnect: UserConnect,
  usermessage: UserMessage,
}
op subscribeToChannel(): SSEStream<ChannelEvents>;

Note that the terminal event is NOT present, this is ok because it's an invention of some APIs, and is not part of the WHATWG spec. It should remain optional.

Here a couple of things are worth noting:

  • imports for typespec streams/sse/events are added
  • a using for SSE is added
  • a @TypeSpec.Events.events decorator is added to the union type
  • the return type of the operation is now SSEStream<ChannelEvents> (instead of ChannelEvents)
  • The union type discriminator values are obtained by conventions based on the event properties in the schema (name is a convention)
  • the union type member types are defined by convention by the schema of the data property, and the fact the content media type is application/json

Scenario 2: with terminal events

Given the following OpenAPI Response object.

content:
  description: A request body to add a stream of typed data.
  required: true
  content:
    text/event-stream:
      itemSchema:
        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:
                  type: object
                  required: [username]
                  properties:
                    username:
                      type: string
          - properties:
              event:
                const: usermessage
              data:
                contentMediaType: application/json
                contentSchema:
                  type: object
                  required: [text]
                  properties:
                    text:
                      type: string

I'd expect aresulting TypeSpec description looking like this.

import "@typespec/streams";
import "@typespec/sse";
import "@typespec/events";

using SSE;

model UserConnect {
  username: string;
}
model UserMessage {
  text: string;
}

@TypeSpec.Events.events
union ChannelEvents {
  userconnect: UserConnect,
  usermessage: UserMessage,
  @TypeSpec.Events.contentType("text/plain")
  @TypeSpec.SSE.terminalEvent
  @extension("x-ms-sse-terminal-event", true)
  "[done]",
}
op subscribeToChannel(): SSEStream<ChannelEvents>;

Terminal events are used to circumvent the fact that the only way to terminal an SSE "session" from the service is to terminal the connection, which may cause "errors" on the client side, and is costly (maybe the connection could have been re-used for a subsequent request).

Here a couple of additions are important to note:

  • An extension is introduced in the description to "flag" the terminal event value as special. This is because today there's no placeholder for that information in the OpenAPI specification (see the discussion in the resources)
  • That extension is ALSO maintained as a decorator during the import. This is because we DO NOT want to specialize the terminal event decorator to emit it to avoid basing the emission behaviour on an extension. So the extension decorator is used for the round trip.
  • The contentType decorator value is taken from the contentMediaType in the description.

Emission

We also need to implement emission from those decorators to enable round-trips. The implementation design need to be similar to what's currently done for xml to enable decoupling of the packages:

See the tryImportXml implementation as an example.

When the @events decorator is encountered by the OpenAPI emitter, it should behave similarly to the @discriminator one. A couple of key differences to keep in mind:

  • the implicit discriminator property name is "event" (conventions from SSE specification)
  • the each model of the union should be emitted as a schema for the data sub-property (see examples above)

When the @terminalEvent decorator is encountered by the OpenAPI emitter, no special behavior should happen at this time.

When the @contentType decorator is encountered, it should be used to specify the value of the contentMediaType property (see example above)

When an operation has a return type which is SSEStream<T>, the response media type should be text/event-stream and itemSchema should be used with the information above instead of schema.

Checklist

  • Follow our Code of Conduct
  • Read the docs.
  • Check that there isn't already an issue that request the same feature to avoid creating a duplicate.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions