Struct with tag to enum with payload

Hey folks, I would like some help cleaning up my code :slight_smile: I want to create a more structured way to handle Nats messages and having the code littered with "nested.message.subject.whatever" is not great, as mistakes can be made!

Currently I'm doing this:

#[derive(Debug, Serialize, Deserialize)]
pub enum AccountRequest {
    AccountCreate(CreateAccountRequest),
    AccountGet(Uuid),
}

impl NatsMessage<AccountRequest> for AccountRequest {
    fn subject(&self) -> &str {
        match self {
            AccountRequest::AccountCreate(_) => "account.create",
            AccountRequest::AccountGet(_) => "account.get",
        }
    }

    fn payload(&self) -> Vec<u8> {
        match self {
            AccountRequest::AccountCreate(data) => serde_json::to_vec(data).unwrap(),
            AccountRequest::AccountGet(id) => id.to_string().into_bytes(),
        }
    }

    fn from_message(msg: &async_nats::Message) -> Option<Self> {
        match msg.subject.as_str() {
            "account.create" => {
                serde_json::from_slice::<models::account::CreateAccountRequest>(&msg.payload)
                    .ok()
                    .map(AccountRequest::AccountCreate)
            }
            "account.get" => {
                let id = std::str::from_utf8(&msg.payload).ok();
                id.and_then(|id| Uuid::parse_str(id).ok())
                    .map(AccountRequest::AccountGet)
            }
            _ => None,
        }
    }
}

And my trait declaration here:

pub trait NatsMessage<T> {
    fn subject(&self) -> &str;
    fn payload(&self) -> Vec<u8>;
    fn from_message(msg: &async_nats::Message) -> Option<T>;

    fn to_message(&self) -> async_nats::Message {
        let payload = self.payload();
        let length = payload.len();
        let subject = Subject::from(self.subject());
        async_nats::Message {
            subject,
            reply: None,
            payload: payload.into(),
            headers: None,
            status: None,
            description: None,
            length,
        }
    }
}

But even doing this I have the hardcoded strings duplicated in both from_message and subject methods.

the nats Message struct looks like this

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message {
    /// Subject to which message is published to.
    pub subject: Subject,
    /// Optional reply subject to which response can be published by [crate::Subscriber].
    /// Used for request-response pattern with [crate::Client::request].
    pub reply: Option<Subject>,
    /// Payload of the message. Can be any arbitrary data format.
    pub payload: Bytes,
    /// Optional headers.
    pub headers: Option<HeaderMap>,
    /// Optional Status of the message. Used mostly for internal handling.
    pub status: Option<StatusCode>,
    /// Optional [status][crate::Message::status] description.
    pub description: Option<String>,

    pub length: usize,
}

I'm currently only interested in subject and body, and essentially for a given type of messages I want to ensure that the bytes payload received, can be serialized and deserialized in the correct way. I am also not looking to handle wildcards at the moment.

Is there a way to do it with serde that I'm obviously missing? Or should I rethink the approach and maybe look at (learning) proc macros?

  1. Don't use enums for unrelated requests and responses. If your requests can correspond to a single response only, then they should be separate types (not members of an enum) with the responses as their respective associated types, so you get a type-safe interface.
  2. If you don't want to duplicate string literals across types, pull them out into const items. The names of items are checked at compile time, so you don't get to make typos, either.
  3. I don't think this has anything to do with serde, serialization, or proc-macros.

Thanks for your answer! Diving a bit deeper:

  1. These are not guaranteed to be request / responses, these are messages from a message queue. They come with a Subject struct (identifying the topic) and a binary payload. I would like to parse the binary payload based on the topic it got sent to. E.g if I receive a message on the create topic, the payload expected to be the whole Struct (minus id), but for the delete topic I only need the id.

1.Yep that's possibly what I'll end up doing, however doesn't allow me to generalise the trait so that I can specify topic and type of the payload, and the trait definitions will take care of it automatically.

2.Very possible, surely there are other crates or techniques better suited crates, hence my question on here :smiley:

Do you have any practical suggestion on how to implement a factory like pattern?

I'm not sure what you mean by that. Associated consts exist in the language. You can use traits to imply a functional dependency between a type and a compile-time value.

I still don't get what the enum is for in this case. Don't you want a per-topic typed channel instead?

Delete and edited the previous post cause I messed up the answering.

Would you by any chance be able to jot down some code (or pseudo-code) example so I can understand a bit better how this could help me?

I don't think I want to, as this could potentially create 100s of NATS subscriber across a variety of micro-service instances. (I don't need micro-service architecture, this is mostly a learning exercise)

What I'm trying to understand is, how can I create a data structure / config and trait that would allow me to map a subject string to a type to parse the payload into, so that I could just do (pseudocode)

pub SomeDataStructure
   - "some.subject.string" : SomeTypeA
   - "some.other.subject":  SomeTypeB

impl MyTrait  for SomeDataStructure {}

Where MyTrait is generic enough that allows me to:

  • match against different events based on subject
  • go back and forth between payload as bytes and payload as a concrete rust type (String, Uuid, SomeStruct etc..)

Are you looking for this?

Thank you, yes this definitely helps me understand! Appreciate it :slight_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.