diff --git a/Cargo.toml b/Cargo.toml index 55ba3bd81..5065f3712 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,9 @@ libc = "0.2.81" streaming-iterator = "0.1.5" bitflags = "1.2.1" chrono = {version = "0.4.19", optional = true} +serde = {version = "1.0.118", features = ["derive"], optional = true} +serde_json = {version = "1.0.67", optional = true} +bincode = {version = "1.3.1", optional = true} [dev-dependencies] clap = "~2.33.3" @@ -36,16 +39,12 @@ pkg-config = "0.3" [features] provenance = ["chrono"] +serde_json_metadata = ["serde", "serde_json"] +serde_bincode_metadata = ["serde", "bincode"] [package.metadata.docs.rs] all-features = true -[[example]] -name = "mutation_metadata_bincode" - -[[example]] -name = "mutation_metadata_std" - # Not run during tests [[example]] name = "tree_traversals" diff --git a/examples/mutation_metadata_bincode.rs b/examples/mutation_metadata_bincode.rs deleted file mode 100644 index 7be690dd9..000000000 --- a/examples/mutation_metadata_bincode.rs +++ /dev/null @@ -1,69 +0,0 @@ -use tskit::metadata; -use tskit::TableAccess; - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -pub struct Mutation { - pub effect_size: f64, - pub dominance: f64, - pub origin_time: i32, -} - -// Implement the metadata trait for our mutation -// type. Will will use the standard library for the implementation -// details. -impl metadata::MetadataRoundtrip for Mutation { - fn encode(&self) -> Result, metadata::MetadataError> { - match bincode::serialize(&self) { - Ok(v) => Ok(v), - Err(e) => Err(crate::metadata::MetadataError::RoundtripError { value: Box::new(e) }), - } - } - - fn decode(md: &[u8]) -> Result { - match bincode::deserialize(md) { - Ok(x) => Ok(x), - Err(e) => Err(crate::metadata::MetadataError::RoundtripError { value: Box::new(e) }), - } - } -} - -impl metadata::MutationMetadata for Mutation {} - -pub fn run() { - let mut tables = tskit::TableCollection::new(1000.).unwrap(); - // The simulation generates a mutation: - let m = Mutation { - effect_size: -0.235423, - dominance: 0.5, - origin_time: 1, - }; - - // The mutation's data are included as metadata: - tables - .add_mutation_with_metadata(0, 0, 0, 0.0, None, &m) - .unwrap(); - - // Decoding requres 2 unwraps: - // 1. The first is to handle errors. - // 2. The second is b/c metadata are optional, - // so a row may return None. - let decoded = tables - .mutations() - .metadata::(0.into()) - .unwrap() - .unwrap(); - - // Check that we've made the round trip: - assert_eq!(decoded.origin_time, 1); - assert!((m.effect_size - decoded.effect_size).abs() < f64::EPSILON); - assert!((m.dominance - decoded.dominance).abs() < f64::EPSILON); -} - -#[test] -fn run_test() { - run(); -} - -fn main() { - run(); -} diff --git a/examples/mutation_metadata_std.rs b/examples/mutation_metadata_std.rs deleted file mode 100644 index 4e2af9a13..000000000 --- a/examples/mutation_metadata_std.rs +++ /dev/null @@ -1,75 +0,0 @@ -use tskit::metadata; -use tskit::TableAccess; - -pub struct Mutation { - pub effect_size: f64, - pub dominance: f64, - pub origin_time: i32, -} - -pub fn run() { - let mut tables = tskit::TableCollection::new(1000.).unwrap(); - // The simulation generates a mutation: - let m = Mutation { - effect_size: -0.235423, - dominance: 0.5, - origin_time: 1, - }; - - // The mutation's data are included as metadata: - tables - .add_mutation_with_metadata(0, 0, 0, 0.0, None, &m) - .unwrap(); - - // Decoding requres 2 unwraps: - // 1. The first is to handle errors. - // 2. The second is b/c metadata are optional, - // so a row may return None. - let decoded = tables - .mutations() - .metadata::(0.into()) - .unwrap() - .unwrap(); - - // Check that we've made the round trip: - assert_eq!(decoded.origin_time, 1); - assert!((m.effect_size - decoded.effect_size).abs() < f64::EPSILON); - assert!((m.dominance - decoded.dominance).abs() < f64::EPSILON); -} - -impl metadata::MetadataRoundtrip for Mutation { - fn encode(&self) -> Result, metadata::MetadataError> { - let mut rv = vec![]; - rv.extend(self.effect_size.to_le_bytes().iter().copied()); - rv.extend(self.dominance.to_le_bytes().iter().copied()); - rv.extend(self.origin_time.to_le_bytes().iter().copied()); - Ok(rv) - } - - // NOTE: there is no point in trying to return MetadataError here! - // Internally, split_at asserts that the starting point is less than the - // end of the slice, and then proceeds to an unsafe operation. - // The lack of nice error handling is a big reason to prefer serde! - fn decode(md: &[u8]) -> Result { - use std::convert::TryInto; - let (effect_size_bytes, rest) = md.split_at(std::mem::size_of::()); - let (dominance_bytes, rest) = rest.split_at(std::mem::size_of::()); - let (origin_time_bytes, _) = rest.split_at(std::mem::size_of::()); - Ok(Self { - effect_size: f64::from_le_bytes(effect_size_bytes.try_into().unwrap()), - dominance: f64::from_le_bytes(dominance_bytes.try_into().unwrap()), - origin_time: i32::from_le_bytes(origin_time_bytes.try_into().unwrap()), - }) - } -} - -impl metadata::MutationMetadata for Mutation {} - -#[test] -fn run_test() { - run(); -} - -fn main() { - run(); -} diff --git a/src/_macros.rs b/src/_macros.rs index 982583791..949b40846 100644 --- a/src/_macros.rs +++ b/src/_macros.rs @@ -322,6 +322,66 @@ macro_rules! handle_metadata_return { }; } +/// Implement [`crate::metadata::MetadataRoundtrip`] +/// for a type using `serde_json`. +/// +/// Requires the `serde_json_metadata` feature. +#[cfg(any(doc, feature = "serde_json_metadata"))] +#[macro_export] +macro_rules! serde_json_metadata { + ($structname: ty) => { + impl $crate::metadata::MetadataRoundtrip for $structname { + fn encode(&self) -> Result, $crate::metadata::MetadataError> { + match serde_json::to_string(self) { + Ok(x) => Ok(x.as_bytes().to_vec()), + Err(e) => { + Err($crate::metadata::MetadataError::RoundtripError { value: Box::new(e) }) + } + } + } + + fn decode(md: &[u8]) -> Result { + let value: Result = serde_json::from_slice(md); + match value { + Ok(v) => Ok(v), + Err(e) => { + Err($crate::metadata::MetadataError::RoundtripError { value: Box::new(e) }) + } + } + } + } + }; +} + +/// Implement [`crate::metadata::MetadataRoundtrip`] +/// for a type using `bincode`. +/// +/// Requires the `serde_bincode_metadata` feature. +#[cfg(any(doc, feature = "serde_bincode_metadata"))] +#[macro_export] +macro_rules! serde_bincode_metadata { + ($structname: ty) => { + impl $crate::metadata::MetadataRoundtrip for $structname { + fn encode(&self) -> Result, tskit::metadata::MetadataError> { + match bincode::serialize(&self) { + Ok(x) => Ok(x), + Err(e) => { + Err($crate::metadata::MetadataError::RoundtripError { value: Box::new(e) }) + } + } + } + fn decode(md: &[u8]) -> Result { + match bincode::deserialize(md) { + Ok(x) => Ok(x), + Err(e) => { + Err($crate::metadata::MetadataError::RoundtripError { value: Box::new(e) }) + } + } + } + } + }; +} + #[cfg(test)] mod test { use crate::error::TskitError; diff --git a/src/lib.rs b/src/lib.rs index 034bb4982..49d6fd258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,10 @@ //! //! * `provenance` //! * Enables [`provenance`] +//! * `serde_json_metadata` +//! * Enables [`serde_json_metadata`] macro. +//! * `serde_bincode_metadata` +//! * Enables [`serde_bincode_metadata`] macro. //! //! To add features to your `Cargo.toml` file: //! diff --git a/src/metadata.rs b/src/metadata.rs index 5de253a92..5949e5029 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -14,67 +14,73 @@ use thiserror::Error; /// [bincode](https://crates.io/crates/bincode) will be one of /// the more useful `serde`-related crates. /// -/// # Examples +/// The library provides two macros to facilitate implementing metadata +/// traits: /// -/// ## Mutation metadata +/// * [`serde_json_metadata`] +/// * [`serde_bincode_metadata`] /// -/// ``` -/// use tskit::handle_metadata_return; -/// use tskit::TableAccess; +/// These macros are optional features. +/// The feature names are the same as the macro names /// -/// #[derive(serde::Serialize, serde::Deserialize)] -/// pub struct MyMutation { -/// origin_time: i32, -/// effect_size: f64, -/// dominance: f64, -/// } -/// -/// impl tskit::metadata::MetadataRoundtrip for MyMutation { -/// fn encode(&self) -> Result, tskit::metadata::MetadataError> { -/// handle_metadata_return!(bincode::serialize(&self)) -/// } -/// -/// fn decode(md: &[u8]) -> Result { -/// handle_metadata_return!(bincode::deserialize(md)) -/// } -/// } -/// -/// impl tskit::metadata::MutationMetadata for MyMutation {} -/// -/// let mut tables = tskit::TableCollection::new(100.).unwrap(); -/// let mutation = MyMutation{origin_time: 100, -/// effect_size: -1e-4, -/// dominance: 0.25}; -/// -/// // Add table row with metadata. -/// tables.add_mutation_with_metadata(0, 0, tskit::MutationId::NULL, 100., None, -/// &mutation).unwrap(); -/// -/// // Decode the metadata -/// // The two unwraps are: -/// // 1. Handle Errors vs Option. -/// // 2. Handle the option for the case of no error. -/// // -/// // The .into() reflects the fact that metadata fetching -/// // functions only take a strong ID type, and tskit-rust -/// // adds Into for i32 for all strong ID types. -/// -/// let decoded = tables.mutations().metadata::(0.into()).unwrap().unwrap(); -/// assert_eq!(mutation.origin_time, decoded.origin_time); -/// match decoded.effect_size.partial_cmp(&mutation.effect_size) { -/// Some(std::cmp::Ordering::Greater) => assert!(false), -/// Some(std::cmp::Ordering::Less) => assert!(false), -/// Some(std::cmp::Ordering::Equal) => (), -/// None => panic!("bad comparison"), -/// }; -/// match decoded.dominance.partial_cmp(&mutation.dominance) { -/// Some(std::cmp::Ordering::Greater) => assert!(false), -/// Some(std::cmp::Ordering::Less) => assert!(false), -/// Some(std::cmp::Ordering::Equal) => (), -/// None => panic!("bad comparison"), -/// }; -/// -/// ``` +#[cfg_attr( + feature = "provenance", + doc = r##" +# Examples + +## Mutation metadata encoded as JSON + +``` +use tskit::handle_metadata_return; +use tskit::TableAccess; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct MyMutation { + origin_time: i32, + effect_size: f64, + dominance: f64, +} + +// Implement tskit::metadata::MetadataRoundtrip +tskit::serde_json_metadata!(MyMutation); + +impl tskit::metadata::MutationMetadata for MyMutation {} + +let mut tables = tskit::TableCollection::new(100.).unwrap(); +let mutation = MyMutation{origin_time: 100, + effect_size: -1e-4, + dominance: 0.25}; + +// Add table row with metadata. +tables.add_mutation_with_metadata(0, 0, tskit::MutationId::NULL, 100., None, + &mutation).unwrap(); + +// Decode the metadata +// The two unwraps are: +// 1. Handle Errors vs Option. +// 2. Handle the option for the case of no error. +// +// The .into() reflects the fact that metadata fetching +// functions only take a strong ID type, and tskit-rust +// adds Into for i32 for all strong ID types. + +let decoded = tables.mutations().metadata::(0.into()).unwrap().unwrap(); +assert_eq!(mutation.origin_time, decoded.origin_time); +match decoded.effect_size.partial_cmp(&mutation.effect_size) { + Some(std::cmp::Ordering::Greater) => assert!(false), + Some(std::cmp::Ordering::Less) => assert!(false), + Some(std::cmp::Ordering::Equal) => (), + None => panic!("bad comparison"), +}; +match decoded.dominance.partial_cmp(&mutation.dominance) { + Some(std::cmp::Ordering::Greater) => assert!(false), + Some(std::cmp::Ordering::Less) => assert!(false), + Some(std::cmp::Ordering::Equal) => (), + None => panic!("bad comparison"), +}; +``` +"## +)] pub trait MetadataRoundtrip { fn encode(&self) -> Result, MetadataError>; fn decode(md: &[u8]) -> Result diff --git a/tests/test_metadata.rs b/tests/test_metadata.rs new file mode 100644 index 000000000..6160ee32e --- /dev/null +++ b/tests/test_metadata.rs @@ -0,0 +1,97 @@ +#[cfg(feature = "serde_json_metadata")] +#[cfg(test)] +mod test_json_metadata { + use tskit::metadata; + use tskit::TableAccess; + + #[derive(serde::Serialize, serde::Deserialize, Debug)] + pub struct Mutation { + pub effect_size: f64, + pub dominance: f64, + pub origin_time: i32, + } + + tskit::serde_json_metadata!(Mutation); + + impl metadata::MutationMetadata for Mutation {} + + #[test] + fn test_roundtrip() { + let mut tables = tskit::TableCollection::new(1000.).unwrap(); + // The simulation generates a mutation: + let m = Mutation { + effect_size: -0.235423, + dominance: 0.5, + origin_time: 1, + }; + + // The mutation's data are included as metadata: + tables + .add_mutation_with_metadata(0, 0, 0, 0.0, None, &m) + .unwrap(); + + // Decoding requres 2 unwraps: + // 1. The first is to handle errors. + // 2. The second is b/c metadata are optional, + // so a row may return None. + let decoded = tables + .mutations() + .metadata::(0.into()) + .unwrap() + .unwrap(); + + // Check that we've made the round trip: + assert_eq!(decoded.origin_time, 1); + assert!((m.effect_size - decoded.effect_size).abs() < f64::EPSILON); + assert!((m.dominance - decoded.dominance).abs() < f64::EPSILON); + } +} + +#[cfg(feature = "serde_bincode_metadata")] +#[cfg(test)] +mod test_bincode_metadata { + use tskit::metadata; + use tskit::TableAccess; + + #[derive(serde::Serialize, serde::Deserialize, Debug)] + pub struct Mutation { + pub effect_size: f64, + pub dominance: f64, + pub origin_time: i32, + } + + tskit::serde_bincode_metadata!(Mutation); + + impl metadata::MutationMetadata for Mutation {} + + #[test] + fn test_roundtrip() { + let mut tables = tskit::TableCollection::new(1000.).unwrap(); + // The simulation generates a mutation: + let m = Mutation { + effect_size: -0.235423, + dominance: 0.5, + origin_time: 1, + }; + + // The mutation's data are included as metadata: + tables + .add_mutation_with_metadata(0, 0, 0, 0.0, None, &m) + .unwrap(); + + // Decoding requres 2 unwraps: + // 1. The first is to handle errors. + // 2. The second is b/c metadata are optional, + // so a row may return None. + let decoded = tables + .mutations() + .metadata::(0.into()) + .unwrap() + .unwrap(); + + // Check that we've made the round trip: + assert_eq!(decoded.origin_time, 1); + assert!((m.effect_size - decoded.effect_size).abs() < f64::EPSILON); + assert!((m.dominance - decoded.dominance).abs() < f64::EPSILON); + } +}