diff --git a/src/http_server.rs b/src/http_server.rs index 2216e57..8608c86 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -736,6 +736,31 @@ async fn get_user_notes( Ok(Json(notes)) } +/// SSE `data:` payload の union (#781 Phase 4)。discriminator は SSE の +/// `event:` フィールド (out-of-band) なので untagged oneOf として文書化する。 +/// `event:` 名と variant の対応: +/// note/mention → StreamNote/StreamMentionEvent、note-updated → +/// StreamNoteUpdatedEvent、note-capture-updated → StreamNoteCaptureEvent、 +/// notification → StreamNotificationEvent、main-{eventType} → StreamMainEvent、 +/// chat → StreamChatMessageEvent、chat-deleted / chat-reacted / chat-unreacted → +/// StreamChatMessage{Deleted,Reacted,Unreacted}Event、status → StreamStatusEvent +#[derive(serde::Serialize, ToSchema)] +#[serde(untagged)] +#[allow(dead_code, clippy::large_enum_variant)] // OpenAPI ドキュメント専用の型 +enum SseEventPayload { + Note(crate::streaming::StreamNoteEvent), + Notification(crate::streaming::StreamNotificationEvent), + Mention(crate::streaming::StreamMentionEvent), + Main(crate::streaming::StreamMainEvent), + NoteUpdated(crate::streaming::StreamNoteUpdatedEvent), + NoteCaptureUpdated(crate::streaming::StreamNoteCaptureEvent), + ChatMessage(crate::streaming::StreamChatMessageEvent), + ChatMessageDeleted(crate::streaming::StreamChatMessageDeletedEvent), + ChatMessageReacted(crate::streaming::StreamChatMessageReactedEvent), + ChatMessageUnreacted(crate::streaming::StreamChatMessageUnreactedEvent), + Status(crate::streaming::StreamStatusEvent), +} + #[utoipa::path( get, path = "/api/events", tag = "events", security(("bearer_auth" = [])), @@ -743,9 +768,13 @@ async fn get_user_notes( responses( (status = 200, description = "Server-sent event stream (`text/event-stream`). Each event \ - carries an `event:` type and a JSON `data:` payload. OpenAPI cannot model \ - a streaming body, so the schema is shown as a string.", - content_type = "text/event-stream", body = String), + carries an `event:` type and a JSON `data:` payload typed as \ + `SseEventPayload` — the `event:` name selects the variant \ + (`note` / `mention` / `note-updated` / `note-capture-updated` / \ + `notification` / `main-{eventType}` / `chat` / `chat-deleted` / \ + `chat-reacted` / `chat-unreacted` / `status`). \ + The `type` query param filters by event-name prefix.", + content_type = "text/event-stream", body = SseEventPayload), (status = 401, description = "Unauthorized", body = ApiErrorResponse), ) )] diff --git a/src/models.rs b/src/models.rs index b216820..ab8fa09 100644 --- a/src/models.rs +++ b/src/models.rs @@ -941,7 +941,7 @@ pub struct Channel { pub color: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct ChatMessage { @@ -961,7 +961,7 @@ pub struct ChatMessage { pub reactions: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct ChatMessageReaction { @@ -969,7 +969,7 @@ pub struct ChatMessageReaction { pub reaction: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct ChatReactionUser { @@ -980,7 +980,7 @@ pub struct ChatReactionUser { pub avatar_url: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct ChatUser { @@ -995,7 +995,7 @@ pub struct ChatUser { pub avatar_decorations: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct ChatRoom { @@ -1082,7 +1082,7 @@ impl std::fmt::Debug for AuthResult { /// Adjacent tagging (`updateType` + `body`) preserves the historical wire shape /// `{ updateType: "reacted", body: { ... } }` so the JSON consumers read is /// unchanged while the contract becomes typed end-to-end. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(tag = "updateType", content = "body", rename_all = "camelCase")] pub enum NoteUpdateBody { @@ -1110,7 +1110,7 @@ impl NoteUpdateBody { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct NoteReactedBody { @@ -1123,7 +1123,7 @@ pub struct NoteReactedBody { pub user_id: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(untagged)] pub enum ReactionEmoji { @@ -1131,7 +1131,7 @@ pub enum ReactionEmoji { Code(String), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct NoteUnreactedBody { @@ -1140,7 +1140,7 @@ pub struct NoteUnreactedBody { pub user_id: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct NotePollVotedBody { @@ -1149,7 +1149,7 @@ pub struct NotePollVotedBody { pub user_id: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct NoteDeletedBody { diff --git a/src/streaming.rs b/src/streaming.rs index 674d920..bfdf5a9 100644 --- a/src/streaming.rs +++ b/src/streaming.rs @@ -170,7 +170,7 @@ impl StreamEvent { } /// `stream-status` で報告する接続状態 (#781)。 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "lowercase")] pub enum StreamConnectionState { @@ -179,16 +179,17 @@ pub enum StreamConnectionState { Disconnected, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamNoteEvent { pub account_id: String, pub subscription_id: String, + #[schema(value_type = NormalizedNote)] pub note: Arc, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamNotificationEvent { @@ -197,16 +198,17 @@ pub struct StreamNotificationEvent { pub notification: NormalizedNotification, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamMentionEvent { pub account_id: String, pub subscription_id: String, + #[schema(value_type = NormalizedNote)] pub note: Arc, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamChatMessageEvent { @@ -215,7 +217,7 @@ pub struct StreamChatMessageEvent { pub message: ChatMessage, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamChatMessageDeletedEvent { @@ -235,7 +237,7 @@ struct ChatReactionWsBody { user: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamChatMessageReactedEvent { @@ -246,7 +248,7 @@ pub struct StreamChatMessageReactedEvent { pub user: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamChatMessageUnreactedEvent { @@ -257,7 +259,7 @@ pub struct StreamChatMessageUnreactedEvent { pub user: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamMainEvent { @@ -267,7 +269,7 @@ pub struct StreamMainEvent { pub body: Value, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamNoteUpdatedEvent { @@ -279,7 +281,7 @@ pub struct StreamNoteUpdatedEvent { pub update: NoteUpdateBody, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamNoteCaptureEvent { @@ -289,7 +291,7 @@ pub struct StreamNoteCaptureEvent { pub update: NoteUpdateBody, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub struct StreamStatusEvent {