boatramp-server 0.2.10

boatramp HTTP server + API library (streaming static-site publishing)
Documentation
//! GraphQL subscriptions.
//!
//! A subscription is served as a **graphql-sse** event stream (see
//! [`crate::stream::serve_graphql_subscription`]): the subscription's root field names a
//! messaging topic; a mutation (or any producer) publishes an execution result to that
//! topic, and each is framed as a graphql-sse `next` event so a standard graphql-sse
//! client consumes it directly. This module only needs to *detect* a subscription
//! operation and *derive its topic* — the transport (subscribe, `Last-Event-ID` resume,
//! heartbeat, connection caps, graphql-sse framing) is the `stream` module's job.
//!
//! boatramp stays GraphQL-*aware*, not an engine: the payload published to the topic is
//! the subscription result your producer computes; the host just fans it out (framed).

use async_graphql_parser::types::{
    DocumentOperations, OperationDefinition, OperationType, Selection,
};
use async_graphql_parser::Positioned;

/// If `query` is a subscription operation, return the messaging topic it streams from —
/// its single root field's name (a subscription has exactly one root field per the
/// GraphQL spec). Returns `None` for a query/mutation or a malformed subscription.
pub(crate) fn subscription_topic(query: &str) -> Option<String> {
    let doc = async_graphql_parser::parse_query(query).ok()?;
    let ops: Vec<&OperationDefinition> = match &doc.operations {
        DocumentOperations::Single(op) => vec![&op.node],
        DocumentOperations::Multiple(map) => map.values().map(|op| &op.node).collect(),
    };
    ops.into_iter().find_map(|op| {
        if op.ty != OperationType::Subscription {
            return None;
        }
        match op.selection_set.node.items.first() {
            Some(Positioned {
                node: Selection::Field(field),
                ..
            }) => Some(field.node.name.node.to_string()),
            _ => None,
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_subscription_yields_its_root_field_as_the_topic() {
        assert_eq!(
            subscription_topic("subscription { messageAdded { id body } }"),
            Some("messageAdded".to_string())
        );
        // A named subscription works too.
        assert_eq!(
            subscription_topic("subscription Live { ticks }"),
            Some("ticks".to_string())
        );
    }

    #[test]
    fn a_query_or_mutation_is_not_a_subscription() {
        assert_eq!(subscription_topic("{ me { name } }"), None);
        assert_eq!(subscription_topic("mutation { post(x: 1) }"), None);
        assert_eq!(subscription_topic("query Q { a }"), None);
    }

    #[test]
    fn a_malformed_query_is_not_a_subscription() {
        assert_eq!(subscription_topic("subscription { "), None);
    }
}