dCBOR Tags
As discussed in Part I: CBOR Tags, CBOR tags are a powerful feature of CBOR that provides a space of integers used to "tag" CBOR data items, specifying their type or meaning.
Let's say we wanted to define a tag that identifies a string as holding an ISO 4217 currency code like USD
or EUR
. We could just use a bare string, but if we want our type to be completely self-describing, we can define a tag for it.
As long as you are the only one using that tag, you can choose any integer you want. But if you want your structure to interoperate with other systems, you should use a tag that is registered with IANA, discussed previously here.
For our demonstration we'll use the tag 33000
, which as of this writing is unassigned by IANA.
How would we tag a string as a currency type? Let's start by defining a constant for our tag:
const TAG_CURRENCY_CODE: u64 = 33000;
We now associate our string with the tag by using the to_tagged_value()
method:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
We can extract the tag and the tagged value using try_into_tagged_value()
. The return type is a tuple of a Tag
and the tagged item:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
The reason you have to call value()
on the returned Tag
to get back the numeric value is that a Tag
may also include a human-readable name you can define for your tag. We'll discuss naming tags later in this chapter.
✅ NOTE: A tagged value is the combination of a tag and the value (data item) it tags. But the value of the tag is the integer that identifies the tag.
If we print the diagnostic notation of our tagged value, we can see the tag in the output:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
As shown above, we can always extract the (Tag, CBOR)
tuple from a tagged value, and then compare the tag value to our constant to see whether we want to process it further. But it's a common pattern to expect to find a specific tag in a particular place in a CBOR structure. dcbor
provides a convenience method try_into_expected_tagged_value()
to test the tag value and return an error if it doesn't match. If it succeeds, it returns the tagged value for further processing.
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
Tagging a Complex Structure
Let's say we want to combine our tagged currency code with an amount. Currency amounts can be tricky, because they are expressed as having decimal fractions, but many common floating point values, like 1.1
cannot be represented exactly in binary floating point, meaning that even highly-precise types like f64
can't represent common currency values accurately.
Let's define a new type called DecimalFraction
that holds an integer mantissa and a signed integer exponent representing powers of 10. When negative, the exponent indicates the number of places to the right of the decimal point, so 1.1
would be represented as a mantissa of 11
with an exponent of -1
, and 1.01
would be represented as a mantissa of 101
with an exponent of -2
.
use dcbor::prelude::*;
use crate::tags::TAG_DECIMAL_FRACTION;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DecimalFraction {
pub exponent: i8,
pub mantissa: i64,
}
impl DecimalFraction {
/// Create a new `DecimalFraction` from raw parts.
pub fn new(exponent: i8, mantissa: i64) -> Self {
Self { exponent, mantissa }
}
/// Convert back to `f64`. May lose precision on large exponents.
pub fn to_f64(self) -> f64 {
(self.mantissa as f64) * (10f64).powi(self.exponent as i32)
}
}
impl std::fmt::Display for DecimalFraction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.mantissa == 0 {
return write!(f, "0");
}
let abs_value = self.mantissa.abs();
let is_negative = self.mantissa < 0;
let prefix = if is_negative { "-" } else { "" };
if self.exponent >= 0 {
// For positive exponent, add zeros after the number
write!(f, "{}{}{}", prefix, abs_value, "0".repeat(self.exponent as usize))
} else {
// For negative exponent, insert decimal point
let abs_exp = -self.exponent as usize;
let value_str = abs_value.to_string();
if value_str.len() <= abs_exp {
// Decimal point at the beginning with possible leading zeros
let padding = abs_exp - value_str.len();
write!(f, "{}0.{}{}", prefix, "0".repeat(padding), value_str)
} else {
// Insert decimal point within the number
let decimal_pos = value_str.len() - abs_exp;
let (integer_part, fractional_part) = value_str.split_at(decimal_pos);
write!(f, "{}{}.{}", prefix, integer_part, fractional_part)
}
}
}
}
impl From<DecimalFraction> for CBOR {
fn from(value: DecimalFraction) -> Self {
// Compose the two-element array
let v = vec![value.exponent as i64, value.mantissa].to_cbor();
// Return the tagged array
CBOR::to_tagged_value(TAG_DECIMAL_FRACTION, v)
}
}
impl TryFrom<CBOR> for DecimalFraction {
type Error = dcbor::Error;
fn try_from(cbor: CBOR) -> Result<Self, Self::Error> {
// Decode the tagged array
let item = cbor.try_into_expected_tagged_value(TAG_DECIMAL_FRACTION)?;
// Convert the item to an array
let arr = item.try_into_array()?;
// Validate the length of the array
if arr.len() != 2 {
return Err("Expected a two-element array".into());
}
// Extract the exponent and mantissa
let exponent: i8 = arr[0].clone().try_into()?;
let mantissa: i64 = arr[1].clone().try_into()?;
// Return the DecimalFraction
Ok(DecimalFraction::new(exponent, mantissa))
}
}
#[test]
fn decimal_fraction() {
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.mantissa, 11);
assert_eq!(a.exponent, -1);
assert!((a.to_f64() - 1.1).abs() < f64::EPSILON);
let b = DecimalFraction::new(-2, 101);
assert_eq!(b.mantissa, 101);
assert_eq!(b.exponent, -2);
assert!((b.to_f64() - 1.01).abs() < f64::EPSILON);
}
#[test]
fn decimal_fraction_display() {
// Test zero
let zero = DecimalFraction::new(0, 0);
assert_eq!(zero.to_string(), "0");
// Test positive value with zero exponent
let simple = DecimalFraction::new(0, 42);
assert_eq!(simple.to_string(), "42");
// Test positive values with positive exponent
let pos_exp1 = DecimalFraction::new(2, 5);
assert_eq!(pos_exp1.to_string(), "500");
let pos_exp2 = DecimalFraction::new(3, 123);
assert_eq!(pos_exp2.to_string(), "123000");
// Test negative values with positive exponent
let neg_pos_exp = DecimalFraction::new(1, -42);
assert_eq!(neg_pos_exp.to_string(), "-420");
// Test positive values with negative exponent
let pos_neg_exp1 = DecimalFraction::new(-2, 123);
assert_eq!(pos_neg_exp1.to_string(), "1.23");
let pos_neg_exp2 = DecimalFraction::new(-1, 5);
assert_eq!(pos_neg_exp2.to_string(), "0.5");
let pos_neg_exp3 = DecimalFraction::new(-3, 5);
assert_eq!(pos_neg_exp3.to_string(), "0.005");
// Test negative values with negative exponent
let neg_neg_exp1 = DecimalFraction::new(-2, -123);
assert_eq!(neg_neg_exp1.to_string(), "-1.23");
let neg_neg_exp2 = DecimalFraction::new(-3, -5);
assert_eq!(neg_neg_exp2.to_string(), "-0.005");
// Test boundary cases
let boundary1 = DecimalFraction::new(-9, 123456789);
assert_eq!(boundary1.to_string(), "0.123456789");
let boundary2 = DecimalFraction::new(-1, 1);
assert_eq!(boundary2.to_string(), "0.1");
}
✅ NOTE: We're not showing a lot of the typical boilerplate code here, like the
impl
s forDebug
,Clone
,Display
, and things likenew()
methods. You can find the complete code in the repo for this book.
It turns out that RFC8949 §3.4.4 already defines a CBOR schema for decimal fractions, so we can use that: it's just a two-element array with the exponent first and the mantissa second. It also reserves the tag 4
for decimal fractions, so we can use that as our tag.
const TAG_DECIMAL_FRACTION: u64 = 4;
use dcbor::prelude::*;
use crate::tags::TAG_DECIMAL_FRACTION;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DecimalFraction {
pub exponent: i8,
pub mantissa: i64,
}
impl DecimalFraction {
/// Create a new `DecimalFraction` from raw parts.
pub fn new(exponent: i8, mantissa: i64) -> Self {
Self { exponent, mantissa }
}
/// Convert back to `f64`. May lose precision on large exponents.
pub fn to_f64(self) -> f64 {
(self.mantissa as f64) * (10f64).powi(self.exponent as i32)
}
}
impl std::fmt::Display for DecimalFraction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.mantissa == 0 {
return write!(f, "0");
}
let abs_value = self.mantissa.abs();
let is_negative = self.mantissa < 0;
let prefix = if is_negative { "-" } else { "" };
if self.exponent >= 0 {
// For positive exponent, add zeros after the number
write!(f, "{}{}{}", prefix, abs_value, "0".repeat(self.exponent as usize))
} else {
// For negative exponent, insert decimal point
let abs_exp = -self.exponent as usize;
let value_str = abs_value.to_string();
if value_str.len() <= abs_exp {
// Decimal point at the beginning with possible leading zeros
let padding = abs_exp - value_str.len();
write!(f, "{}0.{}{}", prefix, "0".repeat(padding), value_str)
} else {
// Insert decimal point within the number
let decimal_pos = value_str.len() - abs_exp;
let (integer_part, fractional_part) = value_str.split_at(decimal_pos);
write!(f, "{}{}.{}", prefix, integer_part, fractional_part)
}
}
}
}
impl From<DecimalFraction> for CBOR {
fn from(value: DecimalFraction) -> Self {
// Compose the two-element array
let v = vec![value.exponent as i64, value.mantissa].to_cbor();
// Return the tagged array
CBOR::to_tagged_value(TAG_DECIMAL_FRACTION, v)
}
}
impl TryFrom<CBOR> for DecimalFraction {
type Error = dcbor::Error;
fn try_from(cbor: CBOR) -> Result<Self, Self::Error> {
// Decode the tagged array
let item = cbor.try_into_expected_tagged_value(TAG_DECIMAL_FRACTION)?;
// Convert the item to an array
let arr = item.try_into_array()?;
// Validate the length of the array
if arr.len() != 2 {
return Err("Expected a two-element array".into());
}
// Extract the exponent and mantissa
let exponent: i8 = arr[0].clone().try_into()?;
let mantissa: i64 = arr[1].clone().try_into()?;
// Return the DecimalFraction
Ok(DecimalFraction::new(exponent, mantissa))
}
}
#[test]
fn decimal_fraction() {
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.mantissa, 11);
assert_eq!(a.exponent, -1);
assert!((a.to_f64() - 1.1).abs() < f64::EPSILON);
let b = DecimalFraction::new(-2, 101);
assert_eq!(b.mantissa, 101);
assert_eq!(b.exponent, -2);
assert!((b.to_f64() - 1.01).abs() < f64::EPSILON);
}
#[test]
fn decimal_fraction_display() {
// Test zero
let zero = DecimalFraction::new(0, 0);
assert_eq!(zero.to_string(), "0");
// Test positive value with zero exponent
let simple = DecimalFraction::new(0, 42);
assert_eq!(simple.to_string(), "42");
// Test positive values with positive exponent
let pos_exp1 = DecimalFraction::new(2, 5);
assert_eq!(pos_exp1.to_string(), "500");
let pos_exp2 = DecimalFraction::new(3, 123);
assert_eq!(pos_exp2.to_string(), "123000");
// Test negative values with positive exponent
let neg_pos_exp = DecimalFraction::new(1, -42);
assert_eq!(neg_pos_exp.to_string(), "-420");
// Test positive values with negative exponent
let pos_neg_exp1 = DecimalFraction::new(-2, 123);
assert_eq!(pos_neg_exp1.to_string(), "1.23");
let pos_neg_exp2 = DecimalFraction::new(-1, 5);
assert_eq!(pos_neg_exp2.to_string(), "0.5");
let pos_neg_exp3 = DecimalFraction::new(-3, 5);
assert_eq!(pos_neg_exp3.to_string(), "0.005");
// Test negative values with negative exponent
let neg_neg_exp1 = DecimalFraction::new(-2, -123);
assert_eq!(neg_neg_exp1.to_string(), "-1.23");
let neg_neg_exp2 = DecimalFraction::new(-3, -5);
assert_eq!(neg_neg_exp2.to_string(), "-0.005");
// Test boundary cases
let boundary1 = DecimalFraction::new(-9, 123456789);
assert_eq!(boundary1.to_string(), "0.123456789");
let boundary2 = DecimalFraction::new(-1, 1);
assert_eq!(boundary2.to_string(), "0.1");
}
Now we can create a DecimalFraction
and convert it to CBOR, showing the diagnostic notation:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
Because conversion from CBOR to a given type can fail, we implement the TryFrom<CBOR>
trait for our DecimalFraction
type:
use dcbor::prelude::*;
use crate::tags::TAG_DECIMAL_FRACTION;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DecimalFraction {
pub exponent: i8,
pub mantissa: i64,
}
impl DecimalFraction {
/// Create a new `DecimalFraction` from raw parts.
pub fn new(exponent: i8, mantissa: i64) -> Self {
Self { exponent, mantissa }
}
/// Convert back to `f64`. May lose precision on large exponents.
pub fn to_f64(self) -> f64 {
(self.mantissa as f64) * (10f64).powi(self.exponent as i32)
}
}
impl std::fmt::Display for DecimalFraction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.mantissa == 0 {
return write!(f, "0");
}
let abs_value = self.mantissa.abs();
let is_negative = self.mantissa < 0;
let prefix = if is_negative { "-" } else { "" };
if self.exponent >= 0 {
// For positive exponent, add zeros after the number
write!(f, "{}{}{}", prefix, abs_value, "0".repeat(self.exponent as usize))
} else {
// For negative exponent, insert decimal point
let abs_exp = -self.exponent as usize;
let value_str = abs_value.to_string();
if value_str.len() <= abs_exp {
// Decimal point at the beginning with possible leading zeros
let padding = abs_exp - value_str.len();
write!(f, "{}0.{}{}", prefix, "0".repeat(padding), value_str)
} else {
// Insert decimal point within the number
let decimal_pos = value_str.len() - abs_exp;
let (integer_part, fractional_part) = value_str.split_at(decimal_pos);
write!(f, "{}{}.{}", prefix, integer_part, fractional_part)
}
}
}
}
impl From<DecimalFraction> for CBOR {
fn from(value: DecimalFraction) -> Self {
// Compose the two-element array
let v = vec![value.exponent as i64, value.mantissa].to_cbor();
// Return the tagged array
CBOR::to_tagged_value(TAG_DECIMAL_FRACTION, v)
}
}
impl TryFrom<CBOR> for DecimalFraction {
type Error = dcbor::Error;
fn try_from(cbor: CBOR) -> Result<Self, Self::Error> {
// Decode the tagged array
let item = cbor.try_into_expected_tagged_value(TAG_DECIMAL_FRACTION)?;
// Convert the item to an array
let arr = item.try_into_array()?;
// Validate the length of the array
if arr.len() != 2 {
return Err("Expected a two-element array".into());
}
// Extract the exponent and mantissa
let exponent: i8 = arr[0].clone().try_into()?;
let mantissa: i64 = arr[1].clone().try_into()?;
// Return the DecimalFraction
Ok(DecimalFraction::new(exponent, mantissa))
}
}
#[test]
fn decimal_fraction() {
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.mantissa, 11);
assert_eq!(a.exponent, -1);
assert!((a.to_f64() - 1.1).abs() < f64::EPSILON);
let b = DecimalFraction::new(-2, 101);
assert_eq!(b.mantissa, 101);
assert_eq!(b.exponent, -2);
assert!((b.to_f64() - 1.01).abs() < f64::EPSILON);
}
#[test]
fn decimal_fraction_display() {
// Test zero
let zero = DecimalFraction::new(0, 0);
assert_eq!(zero.to_string(), "0");
// Test positive value with zero exponent
let simple = DecimalFraction::new(0, 42);
assert_eq!(simple.to_string(), "42");
// Test positive values with positive exponent
let pos_exp1 = DecimalFraction::new(2, 5);
assert_eq!(pos_exp1.to_string(), "500");
let pos_exp2 = DecimalFraction::new(3, 123);
assert_eq!(pos_exp2.to_string(), "123000");
// Test negative values with positive exponent
let neg_pos_exp = DecimalFraction::new(1, -42);
assert_eq!(neg_pos_exp.to_string(), "-420");
// Test positive values with negative exponent
let pos_neg_exp1 = DecimalFraction::new(-2, 123);
assert_eq!(pos_neg_exp1.to_string(), "1.23");
let pos_neg_exp2 = DecimalFraction::new(-1, 5);
assert_eq!(pos_neg_exp2.to_string(), "0.5");
let pos_neg_exp3 = DecimalFraction::new(-3, 5);
assert_eq!(pos_neg_exp3.to_string(), "0.005");
// Test negative values with negative exponent
let neg_neg_exp1 = DecimalFraction::new(-2, -123);
assert_eq!(neg_neg_exp1.to_string(), "-1.23");
let neg_neg_exp2 = DecimalFraction::new(-3, -5);
assert_eq!(neg_neg_exp2.to_string(), "-0.005");
// Test boundary cases
let boundary1 = DecimalFraction::new(-9, 123456789);
assert_eq!(boundary1.to_string(), "0.123456789");
let boundary2 = DecimalFraction::new(-1, 1);
assert_eq!(boundary2.to_string(), "0.1");
}
Now we can round-trip our tagged value, converting it to CBOR and back to a DecimalFraction
:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
Implementing a Tagged String
We used a tagged string for our currency code, but we can also define a CurrencyCode
type using the newtype pattern. This is a common Rust idiom for creating a new type that wraps an existing type, like String
, and provides additional functionality. In this case, the additional functionality is to implement From<CurrencyCode> for CBOR
and TryFrom<CBOR> for CurrencyCode
.
use dcbor::prelude::*;
use crate::TAG_CURRENCY_CODE;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CurrencyCode(String);
impl CurrencyCode {
pub fn new(code: &str) -> Self {
Self(code.into())
}
pub fn code(&self) -> &str {
&self.0
}
}
impl From<CurrencyCode> for CBOR {
fn from(value: CurrencyCode) -> Self {
CBOR::to_tagged_value(TAG_CURRENCY_CODE, value.0)
}
}
impl TryFrom<CBOR> for CurrencyCode {
type Error = dcbor::Error;
fn try_from(cbor: CBOR) -> Result<Self, Self::Error> {
let value = cbor.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?;
let currency_code: String = value.try_into()?;
Ok(CurrencyCode(currency_code))
}
}
impl std::fmt::Display for CurrencyCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
Now we can round-trip our CurrencyCode
the same way we did with DecimalFraction
:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
Combining the Two Types
Originally we set out to create a structure that combined a currency code with a decimal fraction: CurrencyAmount
. We'd also like this structure to have its own tag, so we'll use 33001
, which is also unassigned by IANA as of this writing.
const TAG_CURRENCY_AMOUNT: u64 = 33001;
Now that we have completely reusable constituents, we can define CurrencyAmount
as a type that consists of a CurrencyCode
and a DecimalFraction
.
use dcbor::prelude::*;
use crate::{ CurrencyCode, DecimalFraction, TAG_CURRENCY_AMOUNT };
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CurrencyAmount(CurrencyCode, DecimalFraction);
impl CurrencyAmount {
pub fn new(currency: CurrencyCode, amount: DecimalFraction) -> Self {
Self(currency, amount)
}
pub fn currency(&self) -> &CurrencyCode {
&self.0
}
pub fn amount(&self) -> &DecimalFraction {
&self.1
}
}
impl From<CurrencyAmount> for CBOR {
fn from(value: CurrencyAmount) -> Self {
let v = vec![value.currency().to_cbor(), value.amount().to_cbor()].to_cbor();
CBOR::to_tagged_value(TAG_CURRENCY_AMOUNT, v)
}
}
impl TryFrom<CBOR> for CurrencyAmount {
type Error = dcbor::Error;
fn try_from(cbor: CBOR) -> Result<Self, Self::Error> {
let item = cbor.try_into_expected_tagged_value(TAG_CURRENCY_AMOUNT)?;
let arr = item.try_into_array()?;
if arr.len() != 2 {
return Err("Expected a two-element array".into());
}
let currency: CurrencyCode = arr[0].clone().try_into()?;
let amount: DecimalFraction = arr[1].clone().try_into()?;
Ok(CurrencyAmount(currency, amount))
}
}
impl std::fmt::Display for CurrencyAmount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.currency(), self.amount())
}
}
Notice that in the above example, we're able to call the to_cbor()
method on the CurrencyCode
and DecimalFraction
types, because the dcbor
library includes a blanket implementation for another trait called CBOREncodable
, which automatically applies to any type that implements Into<CBOR>
and Clone
. (We implemented From<CurrencyCode> for CBOR
and From<DecimalFraction> for CBOR
which also implicitly implement the Into<CBOR>
trait, so we get the CBOREncodable
trait for free.)
The CBOREncodable
trait gives us the to_cbor()
method, which can be called on a &self
(reference to self) unlike the into()
method, which consumes the value. It also gives us the to_cbor_data()
method, which returns the final, serialized CBOR data as a Vec<u8>
.
This use of blanket implementations is a common Rust idiom, similar to how types that implement the Display
trait automatically implement the ToString
trait and hence gain the to_string()
method.
Now with all the pieces in place, we can do a full round-trip of our CurrencyAmount
type:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn currency_amount_no_names() -> Result<()> {
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation
let expected_diagnostic = r#"
33001(
[
33000("USD"),
4(
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data
let expected_hex = r#"
d9 80e9 # tag(33001)
82 # array(2)
d9 80e8 # tag(33000)
63 # text(3)
555344 # "USD"
c4 # tag(4)
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
Named Tags
As mentioned, a CBOR tag is just an integer, and that integer is all that is ever serialized to the binary stream. But the dcbor
library allows you to associate a human-readable name with a tag, which can be useful for debugging and documentation. The dcbor
library provides a macro for defining compile-time constants for tags and their names:
use dcbor::prelude::*;
const_cbor_tag!(4, DECIMAL_FRACTION, "DecimalFraction");
const_cbor_tag!(33000, CURRENCY_CODE, "CurrencyCode");
const_cbor_tag!(33001, CURRENCY_AMOUNT, "CurrencyAmount");
pub fn register_tags() {
with_tags_mut!(|tags_store: &mut TagsStore| {
tags_store.insert_all(vec![
cbor_tag!(DECIMAL_FRACTION),
cbor_tag!(CURRENCY_CODE),
cbor_tag!(CURRENCY_AMOUNT),
]);
});
}
These macro invocations are a concise equivalent to the following code:
const TAG_DECIMAL_FRACTION: u64 = 4;
const TAG_NAME_DECIMAL_FRACTION: &str = "DecimalFraction";
const TAG_CURRENCY_CODE: u64 = 33000;
const TAG_NAME_CURRENCY_CODE: &str = "CurrencyCode";
const TAG_CURRENCY_AMOUNT: u64 = 33001;
const TAG_NAME_CURRENCY_AMOUNT: &str = "CurrencyAmount";
To make these names available to runtime calls like CBOR::diagnostic_annotated
and CBOR::hex_annotated
, we need to register them once at the start of our program:
use dcbor::prelude::*;
const_cbor_tag!(4, DECIMAL_FRACTION, "DecimalFraction");
const_cbor_tag!(33000, CURRENCY_CODE, "CurrencyCode");
const_cbor_tag!(33001, CURRENCY_AMOUNT, "CurrencyAmount");
pub fn register_tags() {
with_tags_mut!(|tags_store: &mut TagsStore| {
tags_store.insert_all(vec![
cbor_tag!(DECIMAL_FRACTION),
cbor_tag!(CURRENCY_CODE),
cbor_tag!(CURRENCY_AMOUNT),
]);
});
}
The cbor_tag!
macro is actually doing the work of creating the Tag
instances for us, using the same naming convention as the constants defined using the const_cbor_tag!
macro. The with_tags_mut!
macro provides writable, thread-safe access to the global tag registry.
Here's the same example as before, but calling register_tags()
at the start of the program. Now both output formats include the human-readable names for the tags:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
A Note About the Debug
and Display
implementations on CBOR
You've been learning about calls like CBOR::diagnostic_annotated()
and CBOR::hex_annotated()
, which are used to print the CBOR data in a human-readable format, and CBOR::to_cbor_data()
, which returns the raw CBOR data as a Vec<u8>
.
These methods are useful for debugging (and of course serializing your CBOR), but they are not the same as the Debug
and Display
traits, and it's also important to understand the difference between how these trait outputs are formatted on your original structures, versus how they are formatted on the CBOR
type:
use cbor_book::*;
use anyhow::Result;
use dcbor::prelude::*;
#[test]
#[rustfmt::skip]
fn example_2() -> Result<()> {
let usd = CBOR::to_tagged_value(TAG_CURRENCY_CODE, "USD");
let (tag, item) = usd.clone().try_into_tagged_value()?;
assert_eq!(tag.value(), TAG_CURRENCY_CODE);
assert_eq!(item.try_into_text()?, "USD");
let diagnostic = usd.diagnostic();
let expected_diagnostic = r#"
33000("USD")
"#.trim();
assert_eq!(diagnostic, expected_diagnostic);
let item = usd
.try_into_expected_tagged_value(TAG_CURRENCY_CODE)?
.try_into_text()?;
assert_eq!(item, "USD");
Ok(())
}
// 33000("USD")
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor() {
let a = DecimalFraction::new(-1, 11);
let cbor = a.to_cbor();
assert_eq!(cbor.diagnostic(), r#"
4(
[-1, 11]
)
"#.trim());
}
#[test]
#[rustfmt::skip]
fn decimal_fraction_cbor_roundtrip() -> Result<()> {
// Create a DecimalFraction
let a = DecimalFraction::new(-1, 11);
assert_eq!(a.to_string(), "1.1");
// Convert to CBOR
let cbor = a.clone().to_cbor();
// Convert back to DecimalFraction
let b: DecimalFraction = cbor.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(a, b);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_code_cbor() -> Result<()> {
let usd = CurrencyCode::new("USD");
let cbor = usd.to_cbor();
let usd2: CurrencyCode = cbor.try_into()?;
assert_eq!(usd, usd2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn currency_amount_cbor_named() -> Result<()> {
// Register our tags first thing
register_tags();
// Create a CurrencyAmount
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
assert_eq!(currency_amount.to_string(), "USD 1.1");
// Convert to CBOR
let cbor = currency_amount.to_cbor();
// Check the diagnostic notation, now with named tags
let expected_diagnostic = r#"
33001( / CurrencyAmount /
[
33000("USD"), / CurrencyCode /
4( / DecimalFraction /
[-1, 11]
)
]
)
"#.trim();
assert_eq!(cbor.diagnostic_annotated(), expected_diagnostic);
// Convert to binary CBOR data
let data = cbor.to_cbor_data();
// Check the hex representation of the binary data, now with named tags
let expected_hex = r#"
d9 80e9 # tag(33001) CurrencyAmount
82 # array(2)
d9 80e8 # tag(33000) CurrencyCode
63 # text(3)
555344 # "USD"
c4 # tag(4) DecimalFraction
82 # array(2)
20 # negative(-1)
0b # unsigned(11)
"#.trim();
assert_eq!(cbor.hex_annotated(), expected_hex);
// Convert back to CBOR
let cbor2 = CBOR::try_from_data(data)?;
// Convert back to CurrencyAmount
let currency_amount2: CurrencyAmount = cbor2.try_into()?;
// Check that the original and round-tripped values are equal
assert_eq!(currency_amount, currency_amount2);
Ok(())
}
#[test]
#[rustfmt::skip]
fn debug_and_display_formats() -> Result<()> {
let currency_amount = CurrencyAmount::new(
CurrencyCode::new("USD"),
DecimalFraction::new(-1, 11)
);
//
// Using the `Debug` implementation on `CurrencyAmount`
//
let expected_debug = r#"
CurrencyAmount(CurrencyCode("USD"), DecimalFraction { exponent: -1, mantissa: 11 })
"#.trim();
assert_eq!(format!("{:?}", currency_amount), expected_debug);
//
// Using the `Display` implementation on `CurrencyAmount`
//
let expected_display = r#"
USD 1.1
"#.trim();
assert_eq!(format!("{}", currency_amount), expected_display);
let cbor = currency_amount.to_cbor();
//
// Using the `Debug` implementation on `CBOR`
//
let expected_debug_cbor = r#"
tagged(33001, array([tagged(33000, text("USD")), tagged(4, array([negative(-1), unsigned(11)]))]))
"#.trim();
assert_eq!(format!("{:?}", cbor), expected_debug_cbor);
//
// Using the `Display` implementation on `CBOR`
//
let expected_display_cbor = r#"
33001([33000("USD"), 4([-1, 11])])
"#.trim();
assert_eq!(format!("{}", cbor), expected_display_cbor);
Ok(())
}
- The
Debug
trait onCurrencyAmount
is just the defaultDebug
implementation for a struct, which prints the field names and values in a human-readable format. - The
Display
trait onCurrencyAmount
is a custom implementation that formats the value as a string with the currency code and amount. - The
Debug
trait onCBOR
is a nested symbolic representation of the CBOR major types and values. - The
Display
trait onCBOR
is the same as would be returned byCBOR::diagnostic_flat()
, which is valid diagnostic notation all on one line.
Each of these formats is useful in its own way, so knowing when to use each one will help you get the most out of the dcbor
library.