cakekindel / slack-blocks-rs

slack messages at warp speed!
https://docs.rs/slack-blocks/latest
Apache License 2.0
16 stars 1 forks source link

blox! relies on static lifetimes #169

Open Mattsi-Jansky opened 1 year ago

Mattsi-Jansky commented 1 year ago

I thought I'd try swap out slack-morphism's slack blocks for slack-block-rs but I hit a bit of a snag. I'll start with the code producing the issue, though most details aren't significant:

#[tokio::test]
#[serial]
async fn should_initiate_socket_mode_connection() {
    let websocket_server = async move {
        let mut stream = start_websocket_server().await;
        stream
            .send(Message::Text(String::from(FAKE_HELLO_MESSAGE)))
            .await
            .unwrap();
        stream
            .send(Message::Text(String::from(FAKE_SLACK_TEXT_MESSAGE)))
            .await
            .unwrap();
    };
    let handle = tokio::spawn(websocket_server);
    let builder = TestClientBuilder::new("should_initiate_socket_mode_connection");
    let client = builder.new_client();
    let mut listener = client.connect_to_socket_mode().await.unwrap();

    let result = listener.next().await;
    assert_eq!(result.unwrap(), SocketMessage::Hello {});
    let result = listener.next().await;
    assert_eq!(
        result.unwrap(),
        SocketMessage::Event {
            envelope_id: String::from("fake-enve-lope-i-d"),
            payload: Payload {
                event: Event {
                    id: "1686321337.206879".to_string(),
                    event_type: "message".to_string(),
                    text: Some("test".to_string()),
                    user: Some("F4K3USER1D".to_string()),
                    blocks: vec![Block::Section(blox! {
                        <section_block>
                            <text kind=plain>"test"</text>
                        </section_block>
                    })],
                    channel: Some("F4K3CH4NN3L1D".to_string()),
                    channel_type: Some("im".to_string()),
                }
            }
        }
    );
    handle.abort();
}

It's a simple test with a bunch of setup. The important part is that we're getting a result from a socket (listener.next()) and comparing it to an expected result, part of which is built using blox!. That results in this compile error:

error[E0597]: `*listener` does not live long enough
  --> client/tests/web_sockets.rs:47:18
   |
47 |       let result = listener.next().await;
   |                    ^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
58 |                       blocks: vec![Block::Section(blox! {
   |  _____________________________-
59 | |                         <section_block>
60 | |                             <text kind=plain>"test"</text>
61 | |                         </section_block>
62 | |                     })],
   | |_______________________- cast requires that `*listener` is borrowed for `'static`
...
70 |   }
   |   - `*listener` dropped here while still borrowed

It's a weird compile error. It is definitely caused by blox!. If I replace the vector with vec![Block::Divider], it works. Using a builder also works.

If I've understood what is happening here correctly, I think it is that: SectionBlock relies on lifetimes and mox is automatically providing a 'static lifetime. Which makes sense, because we're providing this data statically. But result is not using static lifetimes. And I'll often be receiving data from sockets that will only last a short time, certainly not until the end of the program.

Have I understood this right? Is there no way to use blox! without static lifetimes?

And if I may ask as an aside, why use so many generic lifetime parameters anyway? That's a genuine question, not intended as a criticism, I'm just trying to learn more about Rust patterns. It seems like if the models didn't use str and Cow they wouldn't need explicit lifetimes. So what was the intention behind using Cow and str rather than Box or Arc and String? Was it to reduce allocations?

cakekindel commented 1 year ago

And if I may ask as an aside, why use so many generic lifetime parameters anyway? That's a genuine question, not intended as a criticism, I'm just trying to learn more about Rust patterns. It seems like if the models didn't use str and Cow they wouldn't need explicit lifetimes. So what was the intention behind using Cow and str rather than Box or Arc and String? Was it to reduce allocations?

It's been a while since I've touched this project but IIRC yes! the intention was that if you don't explicitly need to store heap-allocated strings in the blocks then they won't force you to - since they never actually manipulate your text

cakekindel commented 1 year ago

Have I understood this right? Is there no way to use blox! without static lifetimes?

hmm.. I'm fairly confident your usecase should be possible, and hopefully without having to think too much about lifetimes yourself - could you try moving the block expression to a local and providing the inferred type?

a la

    let result = listener.next().await;

    // expected `___`, found `()`
    let blocks: () = vec![Block::Section(blox! {
                        <section_block>
                            <text kind=plain>"test"</text>
                        </section_block>
                    })];
    assert_eq!(
        result.unwrap(),
        SocketMessage::Event {
            envelope_id: String::from("fake-enve-lope-i-d"),
            payload: Payload {
                event: Event {
                    id: "1686321337.206879".to_string(),
                    event_type: "message".to_string(),
                    text: Some("test".to_string()),
                    user: Some("F4K3USER1D".to_string()),
                    blocks,
                    channel: Some("F4K3CH4NN3L1D".to_string()),
                    channel_type: Some("im".to_string()),
                }
            }
        }
    );