Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ resolver = '2'
members = [
"crates/swss-common",
"crates/swss-common-testing",
"crates/sonicdb",
"crates/sonicdb-derive",
]
exclude = []

Expand Down Expand Up @@ -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" }
23 changes: 23 additions & 0 deletions crates/sonicdb-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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

143 changes: 143 additions & 0 deletions crates/sonicdb-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)
}
26 changes: 26 additions & 0 deletions crates/sonicdb-derive/tests/basic.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
attr2: Option<String>,
}

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");
}
20 changes: 20 additions & 0 deletions crates/sonicdb/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions crates/sonicdb/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use swss_common::KeyOpFieldValues;

/// Trait for objects that can be stored in a Sonic DB table.
pub trait SonicDbTable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SonicDbTable seems a combination of SonicDBInfo and Table. Is this really necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is for a totally different purpose and also provides different info. The primary usage is to be used along with sonicdb-derive, which is to annotate structs for sonicdb tables. Here is one usage https://github.com/sonic-net/sonic-dash-ha/blob/d14d54bb1f1f81307e8ddd76905bbf57a9546198/crates/hamgrd/src/db_structs.rs#L219. It allows designer adding table meta data, including table name, db name, protobuf-encoded etc to a struct. Otherwise, we have to maintain the mapping from struct-type using separate data structure, which is not good for maintenance and readability.
SonicDbInfo provides DB level info and Table is for table access which is nothing SonicDbTable about.
In summary, sonicdb and sonicdb-derive allow adding table meta data to a struct, which SonicDBInfo and Table cannot do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qiluo-msft please let me know if you have further questions. thanks

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;
}
Loading