Skip to content

Commit

Permalink
scylla-macros: add skip_name_checks attribute to SerializeCql
Browse files Browse the repository at this point in the history
Introduce an attribute to the `SerializeCql` macro which causes the
generated code to skip checking names of the serialized Rust fields
against the UDT field names. The motivation behind the attribute is to
ease transition off the old `IntoUserType` macro which blindly
serialized the fields of the struct as they are defined in the Rust
code. While it disables the name checks, type checking is still done, so
there is protection against type confusion, at least.
  • Loading branch information
piodul committed Dec 15, 2023
1 parent eaafc96 commit 344112a
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 3 deletions.
9 changes: 9 additions & 0 deletions scylla-cql/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ pub use scylla_macros::ValueList;
/// macro itself, so in those cases the user must provide an alternative path
/// to either the `scylla` or `scylla-cql` crate.
///
/// `#[scylla(skip_name_checks)]
///
/// _Specific only to the `enforce_order` flavor._
///
/// Skips checking Rust field names against names of the UDT fields. With this
/// annotation, the generated implementation will allow mismatch between Rust
/// struct field names and UDT field names, i.e. it's OK if i-th field has a
/// different name in Rust and in the UDT. Fields are still being type-checked.
///
/// # Field attributes
///
/// `#[scylla(rename = "name_in_the_udt")]`
Expand Down
39 changes: 39 additions & 0 deletions scylla-cql/src/types/serialize/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2434,4 +2434,43 @@ mod tests {

assert_eq!(reference, udt);
}

#[derive(SerializeCql, Debug)]
#[scylla(crate = crate, flavor = "enforce_order", skip_name_checks)]
struct TestUdtWithSkippedNameChecks {
a: String,
b: i32,
}

#[test]
fn test_udt_serialization_with_skipped_name_checks() {
let typ = ColumnType::UserDefinedType {
type_name: "typ".to_string(),
keyspace: "ks".to_string(),
field_types: vec![
("a".to_string(), ColumnType::Text),
("x".to_string(), ColumnType::Int),
],
};

let mut reference = Vec::new();
// Total length of the struct is 23
reference.extend_from_slice(&23i32.to_be_bytes());
// Field 'a'
reference.extend_from_slice(&("Ala ma kota".len() as i32).to_be_bytes());
reference.extend_from_slice("Ala ma kota".as_bytes());
// Field 'x'
reference.extend_from_slice(&4i32.to_be_bytes());
reference.extend_from_slice(&42i32.to_be_bytes());

let udt = do_serialize(
TestUdtWithFieldRenameAndEnforceOrder {
a: "Ala ma kota".to_owned(),
b: 42,
},
&typ,
);

assert_eq!(reference, udt);
}
}
36 changes: 33 additions & 3 deletions scylla-macros/src/serialize/cql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ struct Attributes {

#[darling(default)]
flavor: Flavor,

#[darling(default)]
skip_name_checks: bool,
}

impl Attributes {
Expand Down Expand Up @@ -74,7 +77,7 @@ pub fn derive_serialize_cql(tokens_input: TokenStream) -> Result<syn::ItemImpl,
})
.collect::<Result<_, _>>()?;
let ctx = Context { attributes, fields };
ctx.validate()?;
ctx.validate(&input.ident)?;

let gen: Box<dyn Generator> = match ctx.attributes.flavor {
Flavor::MatchByName => Box::new(FieldSortingGenerator { ctx: &ctx }),
Expand All @@ -92,9 +95,31 @@ pub fn derive_serialize_cql(tokens_input: TokenStream) -> Result<syn::ItemImpl,
}

impl Context {
fn validate(&self) -> Result<(), syn::Error> {
fn validate(&self, struct_ident: &syn::Ident) -> Result<(), syn::Error> {
let mut errors = darling::Error::accumulator();

if self.attributes.skip_name_checks {
// Skipping name checks is only available in enforce_order mode
if self.attributes.flavor != Flavor::EnforceOrder {
let err = darling::Error::custom(
"the `skip_name_checks` attribute is only allowed with the `enforce_order` flavor",
)
.with_span(struct_ident);
errors.push(err);
}

// `rename` annotations don't make sense with skipped name checks
for field in self.fields.iter() {
if field.attrs.rename.is_some() {
let err = darling::Error::custom(
"the `rename` annotations don't make sense with `skip_name_checks` attribute",
)
.with_span(&field.ident);
errors.push(err);
}
}
}

// Check for name collisions
let mut used_names = HashMap::<String, &Field>::new();
for field in self.fields.iter() {
Expand Down Expand Up @@ -330,10 +355,15 @@ impl<'a> Generator for FieldOrderedGenerator<'a> {
let rust_field_ident = &field.ident;
let rust_field_name = field.field_name();
let typ = &field.ty;
let name_check_expression: syn::Expr = if !self.ctx.attributes.skip_name_checks {
parse_quote! { field_name == #rust_field_name }
} else {
parse_quote! { true }
};
statements.push(parse_quote! {
match field_iter.next() {
Some((field_name, typ)) => {
if field_name == #rust_field_name {
if #name_check_expression {
let sub_builder = #crate_path::CellValueBuilder::make_sub_writer(&mut builder);
match <#typ as #crate_path::SerializeCql>::serialize(&self.#rust_field_ident, typ, sub_builder) {
Ok(_proof) => {},
Expand Down

0 comments on commit 344112a

Please sign in to comment.