Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serde de/serialize problem #14

Closed
mymtw opened this issue Jan 6, 2025 · 8 comments
Closed

serde de/serialize problem #14

mymtw opened this issue Jan 6, 2025 · 8 comments

Comments

@mymtw
Copy link

mymtw commented Jan 6, 2025

#[serde(rename_all = "snake_case")]
pub enum TransactionStatus {
    Pending,
    InProgress,
    Completed,
    Failed,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct TransactionQueue {
    pub transaction_queue_id: Uuid,
    pub amount: Decimal,
    pub currency: String,
    pub status: TransactionStatus,
    pub created_at: i64,
    pub updated_at: i64,
}

    let transactions_queue = space
        .index("status_created_at")
        .expect("Index with status_created_at exists")
        .select::<TransactionQueue, _>(
            Some(100),
            None,
            Some(IteratorType::All),
            ("pending".to_string(), 0),
        )
        .await?;
        
Error: Decode(DecodingError { kind: Serde(Syntax("invalid type: string \"pending\", expected array, map or int")), location: None })

strange why error? in db rows seems as:

['edd0a06d-903f-4884-9128-bdc95d68552a', '70', 'USD', 'pending', 1736190481, 1736190481]
@mymtw
Copy link
Author

mymtw commented Jan 6, 2025

also it would be too good to use Uuid/decimal as described here
#2

can you check this answer?
3Hren/msgpack-rust#314 (comment)

@Flowneee
Copy link
Owner

Flowneee commented Jan 6, 2025

It comes from issue in this repo you mentioned: UUID does not have standard representation in MessagePack (and in serde model in general) and is being de/serialized according to Serialize logic in uuid crate. For example serialization is done like this

impl Serialize for Uuid {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        if serializer.is_human_readable() {
            serializer.serialize_str(self.hyphenated().encode_lower(&mut Uuid::encode_buffer()))
        } else {
            serializer.serialize_bytes(self.as_bytes())
        }
    }
}

In this case it seems serializer from rmpv is marked as human readable, thus producing string after serialization, which does not conform to Tarantool's expectation, which is special Ext type:

+--------+------------+-----------------+
| MP_EXT | MP_UUID    | UuidValue       |
| = d8   | = 2        | = 16-byte value |
+--------+------------+-----------------+

You can check in picodata's crate how they actually work with UUID.

You need to write either your own wrapper for UUID, which will de/serialize it correctly, or write functions for #[serde(with = "...")] attribute and apply to each field.

@Flowneee
Copy link
Owner

Flowneee commented Jan 6, 2025

This worked for me

#![allow(unused)]

use rmpv::Value;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tarantool_rs::{Connection, ExecutorExt, IteratorType, Tuple};
use serde_bytes::ByteBuf;
use uuid::Uuid;
use tracing::info;

#[derive(Debug, Serialize, Deserialize)]
struct Row {
    name: String,
    #[serde(deserialize_with = "uuid_deserialize")]
    id: Uuid,
    age: u64
}

pub fn uuid_deserialize<'de, D>(deserializer: D) -> Result<Uuid, D::Error>
where
    D: Deserializer<'de>,
{
    let Value::Ext(_, bytes) = serde::Deserialize::deserialize(deserializer)? else {
        return Err(serde::de::Error::custom("Expected ext, found other type"))?;
    };

    Ok(Uuid::from_bytes(
        bytes
            .try_into()
            .map_err(|_| serde::de::Error::custom("Invalid byte array length for UUID"))?
    ))
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    pretty_env_logger::init();

    let conn = Connection::builder().build("127.0.0.1:3301").await?;

    let test_row = Row {name: "From rust".into(), id: Uuid::nil(), age: 32};

    let resp = conn.replace(
        520,
        (test_row.name, Value::Ext(2, test_row.id.into_bytes().to_vec()), test_row.age)
    ).await?;
    
    info!("Insert resp: {resp:?}");
    
    let resp: Vec<Row> = conn.space("test").await?
        .unwrap()
        .select(None, None, Some(tarantool_rs::IteratorType::All), ())
        .await?;

    info!("Space content: {resp:?}");

    Ok(())
}

with space like this

localhost:3301> test:format({{'name', type = 'string'}, {'id', type = 'uuid'}, {'age', type = 'integer'}})

@Flowneee
Copy link
Owner

Flowneee commented Jan 6, 2025

I am going to close this issue as duplicate of #2

@mymtw
Copy link
Author

mymtw commented Jan 7, 2025

also, I forget to say
deserialize for field status: TransactionStatus
not working too
very strange
tarantool db contains field status as string 'pending'

['edd0a06d-903f-4884-9128-bdc95d68552a', '70', 'USD', 'pending', 1736190481, 1736190481]

but status not deserializing to Enum TransactionStatus

Error: Decode(DecodingError { kind: Serde(Syntax("invalid type: string \"pending\", expected array, map or int")), location: None })

looks like here is too need to implement custom deserializer

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum TransactionStatus {
    Pending,
    InProgress,
    Completed,
    Failed,
}

pub fn transaction_status_deserialize<'de, D>(deserializer: D) -> Result<TransactionStatus, D::Error>
where
    D: Deserializer<'de>,
{
    let status = String::deserialize(deserializer)?;
    match status.as_str() {
        "pending" => Ok(TransactionStatus::Pending),
        "in_progress" => Ok(TransactionStatus::InProgress),
        "completed" => Ok(TransactionStatus::Completed),
        "failed" => Ok(TransactionStatus::Failed),
        _ => Err(D::Error::custom(format!(
            "Invalid transaction status: {}",
            status
        ))),
    }
}

And 
....
    #[serde(deserialize_with = "transaction_status_deserialize")]
    pub status: TransactionStatus,
....

@mymtw
Copy link
Author

mymtw commented Jan 7, 2025

for decimal

#[derive(Serialize, Debug, Clone)]
pub struct Amount(Decimal);

pub fn amount_deserialize<'de, D>(deserializer: D) -> Result<Amount, D::Error>
where
    D: Deserializer<'de>,
{
    let value = Value::deserialize(deserializer)?;

    match value {
        Value::Ext(1, bytes) => {
            if bytes.len() < 2 {
                return Err(serde::de::Error::custom("Invalid decimal format: too few bytes"));
            }

            // The first byte is the scale
            let scale = bytes[0] as u32;

            // Remaining bytes are BCD
            let bcd_bytes = &bytes[1..];
            let mut number_str = String::new();

            // Convert BCD to string
            for &byte in bcd_bytes.iter() {
                let first_digit = byte >> 4; // Most significant nibble
                let second_digit = byte & 0x0F; // Least significant nibble

                // Only add digits if they are valid (0-9)
                if first_digit <= 9 {
                    number_str.push_str(&format!("{}", first_digit));
                }
                if second_digit <= 9 {
                    number_str.push_str(&format!("{}", second_digit));
                }
            }

            // Parse the number string into an i64
            let number = number_str.parse::<i64>()
                .map_err(|e| serde::de::Error::custom(format!("Failed to parse BCD to i64: {}", e)))?;

            // Create decimal and set scale
            let mut decimal = Decimal::from_i64(number)
                .ok_or_else(|| serde::de::Error::custom("Failed to create Decimal from number"))?;

            decimal.set_scale(scale)
                .map_err(|e| serde::de::Error::custom(format!("Failed to set scale: {}", e)))?;

            // Debug output for verification
            println!("Bytes: {:?}, Number: {}, Scale: {}, Result: {}",
                     bytes, number, scale, decimal);

            Ok(Amount(decimal))
        },
        _ => {
            Err(serde::de::Error::custom(format!(
                "Expected decimal value (Ext type 1), got: {:?}",
                value
            )))
        }
    }
}

@mymtw
Copy link
Author

mymtw commented Jan 7, 2025

maybe you can help how to serialize decimal ?

@mymtw
Copy link
Author

mymtw commented Jan 7, 2025

serialize decimal

use serde::ser::{Serialize, Serializer};
use rmpv::encode::write_value;
use rmpv::Value;
use rust_decimal::Decimal;
use std::io::Cursor;

#[derive(Debug)]
pub struct Amount(pub Decimal);

impl Serialize for Amount {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Convert decimal to string to process digits
        let dec_str = self.0.to_string();
        let parts: Vec<&str> = dec_str.split('.').collect();
        
        // Calculate scale (number of digits after decimal point)
        let scale = if parts.len() > 1 {
            parts[1].len() as u8
        } else {
            0
        };
        
        // Combine integer and fractional parts and remove any decimal point
        let num_str = dec_str.replace(".", "").replace("-", "");
        let is_negative = self.0.is_sign_negative();
        
        // Calculate required bytes for BCD encoding
        let mut bcd = Vec::new();
        
        // Add scale byte
        bcd.push(scale);
        
        // Convert to BCD
        let digits: Vec<u8> = num_str
            .chars()
            .filter_map(|c| c.to_digit(10))
            .map(|d| d as u8)
            .collect();
        
        let mut i = 0;
        while i < digits.len() {
            let high = digits[i];
            let low = if i + 1 < digits.len() {
                digits[i + 1]
            } else {
                // If we're at the last digit, add the sign nibble
                if is_negative { 0x0b } else { 0x0c }
            };
            
            bcd.push((high << 4) | low);
            i += 2;
        }
        
        // If we had an even number of digits, add an extra byte for the sign
        if digits.len() % 2 == 0 {
            bcd.push(if is_negative { 0x0b } else { 0x0c });
        }

        // Create MessagePack ext value
        let ext = Value::Ext(1, bcd);
        
        // Serialize to bytes
        let mut buf = Vec::new();
        write_value(&mut buf, &ext).map_err(|e| {
            serde::ser::Error::custom(format!("Failed to serialize decimal: {}", e))
        })?;
        
        // Create mutable cursor
        let mut cursor = Cursor::new(buf);
        
        // Deserialize back to rmpv::Value to get correct MessagePack structure
        let value = rmpv::decode::read_value(&mut cursor).map_err(|e| {
            serde::ser::Error::custom(format!("Failed to create MessagePack value: {}", e))
        })?;
        
        // Serialize the final value
        value.serialize(serializer)
    }
}

impl From<Decimal> for Amount {
    fn from(decimal: Decimal) -> Self {
        Amount(decimal)
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants