diff --git a/Cargo.toml b/Cargo.toml index 3d0fcc87c..935ed1d03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ resolver = '2' members = [ "crates/swss-common", "crates/swss-common-testing", + "crates/sonicdb", + "crates/sonicdb-derive", ] exclude = [] @@ -48,6 +50,13 @@ getset = "0.1" lazy_static = "1.4" serial_test = "3.0" +# procedure macro +syn = "2.0" # For parsing Rust code +quote = "1.0" # For generating Rust code +proc-macro2 = "1.0" # For working with procedural macros + # Internal dependencies swss-common = { version = "0.1.0", path = "crates/swss-common" } swss-common-testing = { version = "0.1.0", path = "crates/swss-common-testing" } +sonicdb = { version = "0.1.0", path = "crates/sonicdb" } +sonicdb-derive = { version = "0.1.0", path = "crates/sonicdb-derive" } \ No newline at end of file diff --git a/crates/sonicdb-derive/Cargo.toml b/crates/sonicdb-derive/Cargo.toml new file mode 100644 index 000000000..2c2a63c03 --- /dev/null +++ b/crates/sonicdb-derive/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sonicdb-derive" +version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +documentation.workspace = true +keywords.workspace = true +edition.workspace = true + +[dependencies] +syn.workspace = true # For parsing Rust code +quote.workspace = true # For generating Rust code +proc-macro2.workspace = true # For working with procedural macros +swss-common = { path = "../swss-common" } +sonicdb = { path = "../sonicdb" } + +[lib] +proc-macro = true + +[lints] +workspace = true + diff --git a/crates/sonicdb-derive/src/lib.rs b/crates/sonicdb-derive/src/lib.rs new file mode 100644 index 000000000..6c4e14869 --- /dev/null +++ b/crates/sonicdb-derive/src/lib.rs @@ -0,0 +1,143 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, LitStr}; + +/// Derive macro for sonic-db serialization/deserialization +#[proc_macro_derive(SonicDb, attributes(sonicdb))] +pub fn serde_sonicdb_derive(input: TokenStream) -> TokenStream { + // Parse the input token stream + let input = parse_macro_input!(input as DeriveInput); + + // Get the struct name + let struct_name = &input.ident; + + // Process struct-level drive attributes + let mut table_name: String = "".to_string(); + let mut key_separator: char = 'a'; + let mut db_name: String = "".to_string(); + let mut protobuf_encoded: bool = false; + let mut is_dpu: bool = false; + for attr in &input.attrs { + if attr.path().is_ident("sonicdb") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("table_name") { + let value = meta.value()?; // this parses the `=` + let s: LitStr = value.parse()?; + table_name = s.value(); + Ok(()) + } else if meta.path.is_ident("key_separator") { + let value = meta.value()?; // this parses the `=` + let s: LitStr = value.parse()?; + let value_str = s.value(); + let mut chars = value_str.chars(); + if let (Some(c), None) = (chars.next(), chars.next()) { + key_separator = c; + } else { + return Err(meta.error("key_separator must be a single character")); + } + Ok(()) + } else if meta.path.is_ident("db_name") { + let value = meta.value()?; // this parses the `=` + let s: LitStr = value.parse()?; + db_name = s.value(); + Ok(()) + } else if meta.path.is_ident("is_proto") { + let value = meta.value()?; // this parses the `=` + let proto_bool: LitStr = value.parse()?; + match proto_bool.value().to_lowercase().as_str() { + "true" => protobuf_encoded = true, + _ => protobuf_encoded = false, + }; + Ok(()) + } else if meta.path.is_ident("is_dpu") { + let value = meta.value()?; + let s: LitStr = value.parse()?; + let value_str = s.value(); + is_dpu = match value_str.as_str() { + "true" | "True" | "1" => true, + "false" | "False" | "0" => false, + _ => return Err(meta.error("is_dpu must be a boolean (true/false)")), + }; + Ok(()) + } else { + Err(meta.error("unknown attribute")) + } + }) + .unwrap(); + } + } + if table_name.is_empty() { + panic!("Missing table_name attribute"); + } + if db_name.is_empty() { + panic!("Missing db_name attribute"); + } + if key_separator == 'a' { + panic!("Missing key_separator attribute"); + } + + let convert_pb_to_json_impl = if protobuf_encoded { + quote! { + fn is_proto() -> bool { + true + } + fn convert_pb_to_json(kfv: &mut swss_common::KeyOpFieldValues) { + let value_hex = match kfv.field_values.get("pb") { + Some(v) => v.to_str().ok(), + None => None, + }; + let value_hex = match value_hex { + Some(s) if !s.is_empty() => s, + _ => return, + }; + let value_bytes = match hex::decode(value_hex) { + Ok(bytes) => bytes, + Err(_) => return, + }; + let config = match #struct_name::decode(&*value_bytes) { + Ok(cfg) => cfg, + Err(_) => return, + }; + + let json = match serde_json::to_string(&config) { + Ok(j) => j, + Err(_) => return, + }; + kfv.field_values.clear(); + kfv.field_values.insert("json".to_string(), json.into()); + } + } + } else { + quote! { + fn is_proto() -> bool { + false + } + } + }; + + let is_dpu_value = is_dpu; + + let expanded = quote! { + impl sonicdb::SonicDbTable for #struct_name { + fn key_separator() -> char { + #key_separator + } + + fn table_name() -> &'static str { + #table_name + } + + fn db_name() -> &'static str { + #db_name + } + + fn is_dpu() -> bool { + #is_dpu_value + } + + #convert_pb_to_json_impl + } + }; + + TokenStream::from(expanded) +} diff --git a/crates/sonicdb-derive/tests/basic.rs b/crates/sonicdb-derive/tests/basic.rs new file mode 100644 index 000000000..42143183d --- /dev/null +++ b/crates/sonicdb-derive/tests/basic.rs @@ -0,0 +1,26 @@ +#![allow(unused)] +use sonicdb::SonicDbTable; +use sonicdb_derive::SonicDb; +#[test] +fn test_attributes() { + #[derive(SonicDb)] + #[sonicdb(table_name = "MY_STRUCT", key_separator = ":", db_name = "db1")] + struct MyStruct { + id1: String, + id2: String, + + attr1: Option, + attr2: Option, + } + + let obj = MyStruct { + id1: "id1".to_string(), + id2: "id2".to_string(), + attr1: Some("attr1".to_string()), + attr2: Some("attr2".to_string()), + }; + + assert!(MyStruct::key_separator() == ':'); + assert!(MyStruct::table_name() == "MY_STRUCT"); + assert!(MyStruct::db_name() == "db1"); +} diff --git a/crates/sonicdb/Cargo.toml b/crates/sonicdb/Cargo.toml new file mode 100644 index 000000000..9cb56c39b --- /dev/null +++ b/crates/sonicdb/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "sonicdb" +description = "SONiC DB" +categories = ["network-programming"] +version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +documentation.workspace = true +keywords.workspace = true +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# internal dependencies +swss-common = { path = "../swss-common" } + +[lints] +workspace = true diff --git a/crates/sonicdb/src/lib.rs b/crates/sonicdb/src/lib.rs new file mode 100644 index 000000000..ddeee70bc --- /dev/null +++ b/crates/sonicdb/src/lib.rs @@ -0,0 +1,16 @@ +use swss_common::KeyOpFieldValues; + +/// Trait for objects that can be stored in a Sonic DB table. +pub trait SonicDbTable { + fn key_separator() -> char; + fn table_name() -> &'static str; + fn db_name() -> &'static str; + fn is_proto() -> bool { + false + } + fn convert_pb_to_json(_kfv: &mut KeyOpFieldValues) { + // Default implementation does nothing. + // This can be overridden by the macro to convert protobuf to JSON. + } + fn is_dpu() -> bool; +}