Skip to content

Commit

Permalink
MOD: Improve record Debug implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
threecgreen committed Jan 8, 2024
1 parent 9444242 commit 76654c8
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 27 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## 0.15.0 - TBD
### Enhancements
- Improved `Debug` implementation for all record types
- Prices are formatted as decimals
- Fixed-length strings are formatted as strings
- Bit flag fields are formatted as binary
- Several fields are formatted as enums instead of their raw representations
- Added `--schema` option to `dbn` CLI tool to filter a DBN to a particular schema. This
allows outputting saved live data to CSV
- Allowed passing `--limit` option to `dbn` CLI tool with `--metadata` flag
Expand Down
21 changes: 21 additions & 0 deletions rust/dbn-macros/src/dbn_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use syn::{parenthesized, spanned::Spanned, token, Field, FieldsNamed, Meta};

pub const C_CHAR_ATTR: &str = "c_char";
pub const FIXED_PRICE_ATTR: &str = "fixed_price";
pub const FMT_BINARY: &str = "fmt_binary";
pub const FMT_METHOD: &str = "fmt_method";
pub const INDEX_TS_ATTR: &str = "index_ts";
pub const SKIP_ATTR: &str = "skip";
pub const UNIX_NANOS_ATTR: &str = "unix_nanos";
Expand Down Expand Up @@ -62,6 +64,8 @@ pub fn find_dbn_attr_args(field: &Field) -> syn::Result<Vec<Ident>> {
} else if let Some(i) = meta.path.get_ident() {
if i == C_CHAR_ATTR
|| i == FIXED_PRICE_ATTR
|| i == FMT_BINARY
|| i == FMT_METHOD
|| i == INDEX_TS_ATTR
|| i == SKIP_ATTR
|| i == UNIX_NANOS_ATTR
Expand Down Expand Up @@ -119,6 +123,23 @@ pub fn is_hidden(field: &Field) -> bool {
.any(|id| id == SKIP_ATTR)
}

pub fn find_dbn_debug_attr(field: &Field) -> syn::Result<Option<Ident>> {
let mut args: Vec<_> = find_dbn_attr_args(field)?
.into_iter()
.filter(|id| {
id == C_CHAR_ATTR || id == FIXED_PRICE_ATTR || id == FMT_BINARY || id == FMT_METHOD
})
.collect();
match args.len() {
0 => Ok(None),
1 => Ok(Some(args.pop().unwrap())),
_ => Err(syn::Error::new(
field.span(),
"Passed incompatible format arguments to dbn attr",
)),
}
}

pub fn find_dbn_serialize_attr(field: &Field) -> syn::Result<Option<Ident>> {
let mut args: Vec<_> = find_dbn_attr_args(field)?
.into_iter()
Expand Down
77 changes: 77 additions & 0 deletions rust/dbn-macros/src/debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Field, ItemStruct};

use crate::{
dbn_attr::{
find_dbn_debug_attr, is_hidden, C_CHAR_ATTR, FIXED_PRICE_ATTR, FMT_BINARY, FMT_METHOD,
},
utils::crate_name,
};

pub fn record_debug_impl(input_struct: &ItemStruct) -> TokenStream {
let record_type = &input_struct.ident;
let field_iter = input_struct
.fields
.iter()
.map(|f| format_field(f).unwrap_or_else(|e| e.into_compile_error()));
quote! {
impl ::std::fmt::Debug for #record_type {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let mut debug_struct = f.debug_struct(stringify!(#record_type));
#(#field_iter)*
debug_struct.finish()
}
}
}
}

pub fn derive_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
let input_struct = parse_macro_input!(input as ItemStruct);
let record_type = &input_struct.ident;
let field_iter = input_struct
.fields
.iter()
.map(|f| format_field(f).unwrap_or_else(|e| e.into_compile_error()));
quote! {
impl ::std::fmt::Debug for #record_type {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let mut debug_struct = f.debug_struct(stringify!(#record_type));
#(#field_iter)*
debug_struct.finish()
}
}
}
.into()
}

fn format_field(field: &Field) -> syn::Result<TokenStream> {
let ident = field.ident.as_ref().unwrap();
if is_hidden(field) {
return Ok(quote!());
}
Ok(match find_dbn_debug_attr(field)? {
Some(id) if id == C_CHAR_ATTR => {
quote! { debug_struct.field(stringify!(#ident), &(self.#ident as u8 as char)); }
}
Some(id) if id == FIXED_PRICE_ATTR => {
let crate_name = crate_name();
quote! { debug_struct.field(stringify!(#ident), &#crate_name::pretty::Px(self.#ident)); }
}
Some(id) if id == FMT_BINARY => {
// format as `0b00101010`
quote! { debug_struct.field(stringify!(#ident), &format_args!("{:#010b}", &self.#ident)); }
}
Some(id) if id == FMT_METHOD => {
// Try to use method to format, otherwise fallback on raw value
return Ok(quote! {
match self.#ident() {
Ok(s) => debug_struct.field(stringify!(#ident), &s),
Err(_) => debug_struct.field(stringify!(#ident), &self.#ident),
};
});
}
_ => quote! { debug_struct.field(stringify!(#ident), &self.#ident); },
})
}
5 changes: 4 additions & 1 deletion rust/dbn-macros/src/has_rtype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub fn attribute_macro_impl(
let raw_index_ts = get_raw_index_ts(&input_struct).unwrap_or_else(|e| e.into_compile_error());
let rtypes = args.args.iter();
let crate_name = crate::utils::crate_name();
let impl_debug = crate::debug::record_debug_impl(&input_struct);
quote! (
#input_struct

Expand Down Expand Up @@ -67,11 +68,13 @@ pub fn attribute_macro_impl(
}
}
}

#impl_debug
)
.into()
}

struct Args {
pub(crate) struct Args {
args: Vec<ExprPath>,
span: Span,
}
Expand Down
51 changes: 46 additions & 5 deletions rust/dbn-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use proc_macro::TokenStream;

mod dbn_attr;
mod debug;
mod has_rtype;
mod py_field_desc;
mod serialize;
Expand All @@ -15,6 +16,15 @@ pub fn derive_mock_pyo3(_item: TokenStream) -> TokenStream {
TokenStream::new()
}

/// Dummy derive macro to enable enable the `dbn` helper attribute for record types
/// using the `dbn_record` proc macro but neither `CsvSerialize` nor `JsonSerialize` as
/// helper attributes aren't supported for proc macros alone. See
/// <https://github.com/rust-lang/rust/issues/65823>.
#[proc_macro_derive(DbnAttr, attributes(dbn))]
pub fn dbn_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}

/// Derive macro for CSV serialization. Supports the following `dbn` attributes:
/// - `c_char`: serializes the field as a `char`
/// - `encode_order`: overrides the position of the field in the CSV table
Expand All @@ -31,7 +41,9 @@ pub fn derive_csv_serialize(input: TokenStream) -> TokenStream {
serialize::derive_csv_macro_impl(input)
}

/// Derive macro for JSON serialization. Supports the following `dbn` attributes:
/// Derive macro for JSON serialization.
///
/// Supports the following `dbn` attributes:
/// - `c_char`: serializes the field as a `char`
/// - `fixed_price`: serializes the field as fixed-price, with the output format
/// depending on `PRETTY_PX`
Expand All @@ -46,8 +58,9 @@ pub fn derive_json_serialize(input: TokenStream) -> TokenStream {
serialize::derive_json_macro_impl(input)
}

/// Derive macro for field descriptions exposed to Python. Supports the following `dbn`
/// attributes:
/// Derive macro for field descriptions exposed to Python.
///
/// Supports the following `dbn` attributes:
/// - `c_char`: indicates the field dtype should be a single-character string rather
/// than an integer
/// - `encode_order`: overrides the position of the field in the ordered list
Expand All @@ -59,19 +72,47 @@ pub fn derive_py_field_desc(input: TokenStream) -> TokenStream {
py_field_desc::derive_impl(input)
}

/// Attribute macro that acts like a derive macro for for `HasRType` and
/// `AsRef<[u8]>`.
/// Attribute macro that acts like a derive macro for for `Debug` (with customization),
/// `Record`, `RecordMut`, `HasRType`, `PartialOrd`, and `AsRef<[u8]>`.
///
/// Expects 1 or more paths to `u8` constants that are the RTypes associated
/// with this record.
///
/// Supports the following `dbn` attributes:
/// - `c_char`: format the type as a `char` instead of as a numeric
/// - `fixed_price`: format the integer as a fixed-precision decimal
/// - `fmt_binary`: format as a binary
/// - `fmt_method`: try to format by calling the getter method with the same name as the
/// - `index_ts`: indicates this field is the primary timestamp for the record
/// field. If the getter returns an error, the raw field value will be used
/// - `skip`: won't be included in the `Debug` output
///
/// Note: attribute macros don't support helper attributes on their own. If not deriving
/// `CsvSerialize` or `JsonSerialize`, derive `DbnAttr` to use the `dbn` helper attribute
/// without a compiler error.
#[proc_macro_attribute]
pub fn dbn_record(attr: TokenStream, input: TokenStream) -> TokenStream {
has_rtype::attribute_macro_impl(attr, input)
}

/// Derive macro for Debug representations with the same extensions for DBN records
/// as `dbn_record`.
///
/// Supports the following `dbn` attributes:
/// - `c_char`: format the type as a `char` instead of as a numeric
/// - `fixed_price`: format the integer as a fixed-precision decimal
/// - `fmt_binary`: format as a binary
/// - `fmt_method`: try to format by calling the getter method with the same name as the
/// field. If the getter returns an error, the raw field value will be used
/// - `skip`: won't be included in the `Debug` output
///
/// Note: fields beginning with `_` will automatically be skipped, e.g. `_dummy` isn't
/// included in the `Debug` output.
#[proc_macro_derive(RecordDebug, attributes(dbn))]
pub fn derive_record_debug(input: TokenStream) -> TokenStream {
debug::derive_impl(input)
}

#[cfg(test)]
mod tests {
#[test]
Expand Down
19 changes: 16 additions & 3 deletions rust/dbn/src/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub unsafe fn decode_record_ref<'a>(
///
/// Note: This will be renamed to `InstrumentDefMsg` in DBN version 2.
#[repr(C)]
#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "trivial_copy", derive(Copy))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
Expand Down Expand Up @@ -208,32 +208,43 @@ pub struct InstrumentDefMsgV1 {
#[pyo3(get, set)]
pub channel_id: u16,
/// The currency used for price fields.
#[dbn(fmt_method)]
#[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
pub currency: [c_char; 4],
/// The currency used for settlement, if different from `currency`.
#[dbn(fmt_method)]
#[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
pub settl_currency: [c_char; 4],
/// The strategy type of the spread.
#[dbn(fmt_method)]
#[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
pub secsubtype: [c_char; 6],
/// The instrument raw symbol assigned by the publisher.
#[dbn(encode_order(2))]
#[dbn(encode_order(2), fmt_method)]
pub raw_symbol: [c_char; SYMBOL_CSTR_LEN_V1],
/// The security group code of the instrument.
#[dbn(fmt_method)]
pub group: [c_char; 21],
/// The exchange used to identify the instrument.
#[dbn(fmt_method)]
pub exchange: [c_char; 5],
/// The underlying asset code (product code) of the instrument.
#[dbn(fmt_method)]
pub asset: [c_char; 7],
/// The ISO standard instrument categorization code.
#[dbn(fmt_method)]
pub cfi: [c_char; 7],
/// The type of the instrument, e.g. FUT for future or future spread.
#[dbn(fmt_method)]
pub security_type: [c_char; 7],
/// The unit of measure for the instrument’s original contract size, e.g. USD or LBS.
#[dbn(fmt_method)]
pub unit_of_measure: [c_char; 31],
/// The symbol of the first underlying instrument.
#[dbn(fmt_method)]
pub underlying: [c_char; 21],
/// The currency of [`strike_price`](Self::strike_price).
#[dbn(fmt_method)]
pub strike_price_currency: [c_char; 4],
/// The classification of the instrument.
#[dbn(c_char, encode_order(4))]
Expand Down Expand Up @@ -304,7 +315,7 @@ pub struct InstrumentDefMsgV1 {
///
/// Note: This will be renamed to `SymbolMappingMsg` in DBN version 2.
#[repr(C)]
#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "trivial_copy", derive(Copy))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
Expand All @@ -320,8 +331,10 @@ pub struct SymbolMappingMsgV1 {
#[pyo3(get, set)]
pub hd: RecordHeader,
/// The input symbol.
#[dbn(fmt_method)]
pub stype_in_symbol: [c_char; SYMBOL_CSTR_LEN_V1],
/// The output symbol.
#[dbn(fmt_method)]
pub stype_out_symbol: [c_char; SYMBOL_CSTR_LEN_V1],
// Filler for alignment.
#[doc(hidden)]
Expand Down
2 changes: 1 addition & 1 deletion rust/dbn/src/macros.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Helper macros for working with multiple RTypes, Schemas, and types of records.
// Re-export
pub use dbn_macros::{dbn_record, CsvSerialize, JsonSerialize, PyFieldDesc};
pub use dbn_macros::{dbn_record, CsvSerialize, DbnAttr, JsonSerialize, PyFieldDesc, RecordDebug};

/// Base macro for type dispatch based on rtype.
///
Expand Down
2 changes: 1 addition & 1 deletion rust/dbn/src/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl fmt::Debug for Ts {

impl fmt::Debug for Px {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
f.write_str(&fmt_px(self.0))
}
}

Expand Down
Loading

0 comments on commit 76654c8

Please sign in to comment.