Skip to content

Commit

Permalink
scylla-macros: introduce SerializeCql derive macro
Browse files Browse the repository at this point in the history
Introduce a derive macro which serializes a struct into a UDT.

Unlike the previous IntoUserType, the new macro takes care to match
the struct fields to UDT fields by their names. It does not assume that
the order of the fields in the Rust struct is the same as in the UDT.
  • Loading branch information
piodul committed Oct 20, 2023
1 parent 3c61bff commit 4d35a3c
Show file tree
Hide file tree
Showing 8 changed files with 664 additions and 0 deletions.
8 changes: 8 additions & 0 deletions scylla-cql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ pub mod _macro_internal {
SerializedResult, SerializedValues, Value, ValueList, ValueTooBig,
};
pub use crate::macros::*;

pub use crate::types::serialize::value::{
SerializeCql, UdtSerializationError, UdtSerializationErrorKind, UdtTypeCheckError,
UdtTypeCheckErrorKind,
};
pub use crate::types::serialize::SerializationError;

pub use crate::frame::response::result::ColumnType;
}
62 changes: 62 additions & 0 deletions scylla-cql/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,68 @@ pub use scylla_macros::IntoUserType;
/// #[derive(ValueList)] allows to pass struct as a list of values for a query
pub use scylla_macros::ValueList;

/// Derive macro for the [`SerializeCql`](crate::types::serialize::value::SerializeCql) trait
/// which serializes given Rust structure as a User Defined Type (UDT).
///
/// At the moment, only structs with named fields are supported. The generated
/// implementation of the trait will match the struct fields to UDT fields
/// by name automatically.
///
/// Serialization will fail if there are some fields in the UDT that don't match
/// to any of the Rust struct fields, _or vice versa_.
///
/// In case of failure, either [`UdtTypeCheckError`](crate::types::serialize::value::UdtTypeCheckError)
/// or [`UdtSerializationError`](crate::types::serialize::value::UdtSerializationError)
/// will be returned.
///
/// # Example
///
/// A UDT defined like this:
///
/// ```notrust
/// CREATE TYPE ks.my_udt (a int, b text, c blob);
/// ```
///
/// ...can be serialized using the following struct:
///
/// ```rust
/// # use scylla_cql::macros::SerializeCql;
/// #[derive(SerializeCql)]
/// # #[scylla(crate = scylla_cql)]
/// struct MyUdt {
/// a: i32,
/// b: Option<String>,
/// c: Vec<u8>,
/// }
/// ```
///
/// # Attributes
///
/// `#[scylla(crate = crate_name)]`
///
/// By default, the code generated by the derive macro will refer to the items
/// defined by the driver (types, traits, etc.) via the `::scylla` path.
/// For example, it will refer to the [`SerializeCql`](crate::types::serialize::value::SerializeCql) trait
/// using the following path:
///
/// ```rust,ignore
/// use ::scylla::_macro_internal::SerializeCql;
/// ```
///
/// Most users will simply add `scylla` to their dependencies, then use
/// the derive macro and the path above will work. However, there are some
/// niche cases where this path will _not_ work:
///
/// - The `scylla` crate is imported under a different name,
/// - The `scylla` crate is _not imported at all_ - the macro actually
/// is defined in the `scylla-macros` crate and the generated code depends
/// on items defined in `scylla-cql`.
///
/// It's not possible to automatically resolve those issues in the procedural
/// macro itself, so in those cases the user must provide an alternative path
/// to either the `scylla` or `scylla-cql` crate.
pub use scylla_macros::SerializeCql;

// Reexports for derive(IntoUserType)
pub use bytes::{BufMut, Bytes, BytesMut};

Expand Down
282 changes: 282 additions & 0 deletions scylla-cql/src/types/serialize/value.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use std::fmt::Display;
use std::sync::Arc;

use thiserror::Error;

use crate::frame::response::result::ColumnType;
use crate::frame::value::Value;

Expand All @@ -20,3 +23,282 @@ impl<T: Value> SerializeCql for T {
.map_err(|err| Arc::new(err) as SerializationError)
}
}

/// Returned by the code generated by [`SerializeCql`] macro if the Rust struct
/// does not match the CQL type it was requested to be serialized to.
///
/// Returned by the [`SerializeCql::preliminary_type_check`] method from
/// the trait implementation generated by the macro.
#[derive(Debug, Error)]
#[error("Failed to type check Rust struct {rust_name} as CQL type {matched_with:?}: {kind}")]
pub struct UdtTypeCheckError {
/// Name of the Rust structure that was being serialized.
pub rust_name: String,

/// The CQL type of the bind marker to which the UDT was attempted to be serialized to.
pub matched_with: ColumnType,

/// Detailed infomation about why the type check failed.
pub kind: UdtTypeCheckErrorKind,
}

/// Detailed information about why type checking of the UDT failed.
#[derive(Debug)]
#[non_exhaustive]
pub enum UdtTypeCheckErrorKind {
/// The CQL type that the rust struct was attempted to be deserialized to is not a UDT.
NotUdt,

/// One of the fields that is required to be present by the Rust struct was not present in the CQL UDT type.
MissingField { field_name: String },

/// A field is present in the CQL UDT which does not match to any field of the Rust struct,
/// and we don't know how to handle it.
UnexpectedField { field_name: String },

/// One of the fields failed to type check.
FieldTypeCheckFailed {
field_name: String,
err: SerializationError,
},
}

impl Display for UdtTypeCheckErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UdtTypeCheckErrorKind::NotUdt => write!(f, "the CQL type that the rust struct was attempted to be type checked to is not a UDT"),
UdtTypeCheckErrorKind::MissingField { field_name } => write!(f, "the field {field_name} is missing from the CQL UDT type"),
UdtTypeCheckErrorKind::UnexpectedField { field_name } => write!(f, "the field {field_name} appears in the CQL UDT type but does not in the rust struct"),
UdtTypeCheckErrorKind::FieldTypeCheckFailed { field_name, err } => write!(f, "the field {field_name} failed to type check: {err}"),
}
}
}

/// Returned by the code generated by [`SerializeCql`] macro when a Rust struct
/// fails to be serialized as a CQL value.
///
/// Returned by the [`SerializeCql::serialize`] method from the trait
/// implementation generated by the macro.
#[derive(Debug, Error)]
#[error("Failed to serialize Rust struct {rust_name} as a CQL type {matched_with:?}: {kind}")]
pub struct UdtSerializationError {
/// Name of the Rust structure that was being serialized.
pub rust_name: String,

/// The CQL type of the bind marker to which the UDT was attempted to be serialized to.
pub matched_with: ColumnType,

/// Detailed infomation about why serialization failed.
pub kind: UdtSerializationErrorKind,
}

/// Detailed information about why serialization of the UDT failed.
#[derive(Debug)]
#[non_exhaustive]
pub enum UdtSerializationErrorKind {
/// The CQL type that the rust struct was attempted to be deserialized to is not a UDT.
NotUdt,

/// The UDT in its serialized form exceeds the limit allowed by the CQL protocol
/// ([`i32::MAX`](std::primitive::i32::MAX)).
TooBig { size: usize },

/// One of the fields failed to be serialized.
FieldSerializationFailed {
field_name: String,
err: SerializationError,
},
}

impl Display for UdtSerializationErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UdtSerializationErrorKind::NotUdt => write!(f, "the CQL type that the rust struct was attempted to be deserialized to is not a UDT"),
UdtSerializationErrorKind::TooBig { size } => write!(f, "the serialized size of the rust struct is too big ({} bytes > {} max bytes); it cannot be represented in the serialization format", size, i32::MAX),
UdtSerializationErrorKind::FieldSerializationFailed { field_name, err } => write!(f, "the field {field_name} failed to serialize: {err}"),
}
}
}

#[cfg(test)]
mod tests {
use scylla_macros::SerializeCql;

use super::{SerializeCql, UdtTypeCheckError, UdtTypeCheckErrorKind};
use crate::frame::response::result::{ColumnType, CqlValue};

fn do_serialize<T: SerializeCql>(t: T, typ: &ColumnType) -> Vec<u8> {
T::preliminary_type_check(typ).unwrap();
let mut ret = Vec::new();
t.serialize(typ, &mut ret).unwrap();
ret
}

// Do not remove. It's not used in tests but we keep it here to check that
// we properly ignore warnings about unused variables, unnecessary `mut`s
// etc. that usually pop up when generating code for empty structs.
#[derive(SerializeCql)]
#[scylla(crate = crate)]
struct TestUdtWithNoFields {}

#[derive(SerializeCql, Debug, PartialEq, Eq)]
#[scylla(crate = crate)]
struct TestUdtWithFieldSorting {
a: String,
b: i32,
c: Vec<i64>,
}

#[test]
fn test_udt_serialization_with_field_sorting_correct_order() {
let typ = ColumnType::UserDefinedType {
type_name: "typ".to_string(),
keyspace: "ks".to_string(),
field_types: vec![
("a".to_string(), ColumnType::Text),
("b".to_string(), ColumnType::Int),
(
"c".to_string(),
ColumnType::List(Box::new(ColumnType::BigInt)),
),
],
};

let reference = do_serialize(
&CqlValue::UserDefinedType {
keyspace: "ks".to_string(),
type_name: "typ".to_string(),
fields: vec![
(
"a".to_string(),
Some(CqlValue::Text(String::from("Ala ma kota"))),
),
("b".to_string(), Some(CqlValue::Int(42))),
(
"c".to_string(),
Some(CqlValue::List(vec![
CqlValue::BigInt(1),
CqlValue::BigInt(2),
CqlValue::BigInt(3),
])),
),
],
},
&typ,
);
let udt = do_serialize(
TestUdtWithFieldSorting {
a: "Ala ma kota".to_owned(),
b: 42,
c: vec![1, 2, 3],
},
&typ,
);

assert_eq!(reference, udt);
}

#[test]
fn test_udt_serialization_with_field_sorting_incorrect_order() {
let typ = ColumnType::UserDefinedType {
type_name: "typ".to_string(),
keyspace: "ks".to_string(),
field_types: vec![
// Two first columns are swapped
("b".to_string(), ColumnType::Int),
("a".to_string(), ColumnType::Text),
(
"c".to_string(),
ColumnType::List(Box::new(ColumnType::BigInt)),
),
],
};

let reference = do_serialize(
&CqlValue::UserDefinedType {
keyspace: "ks".to_string(),
type_name: "typ".to_string(),
fields: vec![
// FIXME: UDTs in CqlValue should also honor the order
// For now, it's swapped here as well
("b".to_string(), Some(CqlValue::Int(42))),
(
"a".to_string(),
Some(CqlValue::Text(String::from("Ala ma kota"))),
),
(
"c".to_string(),
Some(CqlValue::List(vec![
CqlValue::BigInt(1),
CqlValue::BigInt(2),
CqlValue::BigInt(3),
])),
),
],
},
&typ,
);
let udt = do_serialize(
TestUdtWithFieldSorting {
a: "Ala ma kota".to_owned(),
b: 42,
c: vec![1, 2, 3],
},
&typ,
);

assert_eq!(reference, udt);
}

#[test]
fn test_udt_serialization_failing_type_check() {
let typ_not_udt = ColumnType::Ascii;

let err = TestUdtWithFieldSorting::preliminary_type_check(&typ_not_udt).unwrap_err();
let err = err.downcast_ref::<UdtTypeCheckError>().unwrap();
assert!(matches!(err.kind, UdtTypeCheckErrorKind::NotUdt));

let typ_without_c = ColumnType::UserDefinedType {
type_name: "typ".to_string(),
keyspace: "ks".to_string(),
field_types: vec![
("a".to_string(), ColumnType::Text),
("b".to_string(), ColumnType::Int),
// Last field is missing
],
};

let err = TestUdtWithFieldSorting::preliminary_type_check(&typ_without_c).unwrap_err();
let err = err.downcast_ref::<UdtTypeCheckError>().unwrap();
assert!(matches!(
err.kind,
UdtTypeCheckErrorKind::MissingField { .. }
));

let typ_unexpected_field = ColumnType::UserDefinedType {
type_name: "typ".to_string(),
keyspace: "ks".to_string(),
field_types: vec![
("a".to_string(), ColumnType::Text),
("b".to_string(), ColumnType::Int),
(
"c".to_string(),
ColumnType::List(Box::new(ColumnType::BigInt)),
),
// Unexpected field
("d".to_string(), ColumnType::Counter),
],
};

let err =
TestUdtWithFieldSorting::preliminary_type_check(&typ_unexpected_field).unwrap_err();
let err = err.downcast_ref::<UdtTypeCheckError>().unwrap();
assert!(matches!(
err.kind,
UdtTypeCheckErrorKind::UnexpectedField { .. }
));

// TODO: Test case for mismatched field types
// Can't do it without proper SerializeCql implementation of field types
}
}
1 change: 1 addition & 0 deletions scylla-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ license = "MIT OR Apache-2.0"
proc-macro = true

[dependencies]
darling = "0.20.0"
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
Loading

0 comments on commit 4d35a3c

Please sign in to comment.