Last active
June 11, 2018 07:51
-
-
Save KodrAus/b88fed867f4028e0deadc06c8966d51a to your computer and use it in GitHub Desktop.
Properties Macro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
This example demonstrates a potential macro for capturing log properties. | |
The macro uses a syntax that's _similar_ to struct literals. The idea is to support | |
extensions to the way properties are captured using attributes. | |
There's a bit of a misalignment between how formatting is communicated in the log | |
message and the contextual properties, but they are a bit different. Args are slurped | |
up into the message using the formatting API whereas properties are exposed as data. | |
Attributes use the following syntax: | |
- `#[log(adapter)]` where `adapter` is a free function in `adapter::map` that takes a | |
generic value `&T` as an argument and returns `impl ToValue`. | |
- `#[log(adapter = state)]` where `adapter` is a free function in `adapter::map_with` that | |
takes a generic value `&T` and `state` `S` and returns `impl ToValue`. | |
There are a few root adapters: | |
- `debug`: formats the property value using its `Debug` implementation | |
- `display`: formats the property using its `Display` implementation | |
There are a few adapters that take additional state: | |
- `fmt`: takes a function that's compatible with one of the `std::fmt` traits and uses | |
it to format the property value | |
- `with`: takes some function that maps a generic value `&T` to some `impl ToValue`. | |
This is an integration point for arbitrary formatters. | |
A downside of using attributes is thst one might expect standard Rust macros to work | |
in the same context, which they currently won't. A proc-macro based solution might be | |
a bit more robust and make it possible to treat the `#[log]` attributes as any other. | |
*/ | |
#[cfg(feature = "erased-serde")] | |
extern crate erased_serde; | |
extern crate serde; | |
extern crate serde_json; | |
#[macro_export] | |
macro_rules! properties( | |
// Do nothing | |
() => {}; | |
// Parse tokens between braces | |
({ $($stream:tt)* }) => {{ | |
__properties_internal!(@ expect_adapter { | |
stream: [$($stream)*] | |
}); | |
}}; | |
); | |
#[macro_export] | |
#[doc(hidden)] | |
macro_rules! __properties_internal( | |
// We're finished parsing | |
(@ expect_adapter { stream: [] }) => { }; | |
// Munch a key identifier from the token stream | |
(@ expect_adapter { | |
stream: [$key:ident $($stream:tt)*] | |
}) => { | |
__properties_internal!(@ expect_value { | |
stream: [$($stream)*], | |
adapter: { | |
kind: default | |
}, | |
key: $key | |
}) | |
}; | |
// Munch an attribute from the token stream | |
(@ expect_adapter { | |
stream: [#[log($adapter:ident)] $($stream:tt)*] | |
}) => { | |
__properties_internal!(@ expect_key { | |
stream: [$($stream)*], | |
adapter: { | |
kind: $adapter | |
} | |
}) | |
}; | |
// Munch an attribute from the token stream | |
(@ expect_adapter { | |
stream: [#[log($adapter_kind:ident = $adapter_state:expr)] $($stream:tt)*] | |
}) => { | |
__properties_internal!(@ expect_key { | |
stream: [$($stream)*], | |
adapter: { | |
kind: $adapter_kind, | |
state: $adapter_state | |
} | |
}) | |
}; | |
// Munch a key as an identifier from the token stream | |
(@ expect_key { | |
stream: [$key:ident $($stream:tt)*], | |
adapter: { $($adapter:tt)* } | |
}) => { | |
__properties_internal!(@ expect_value { | |
stream: [$($stream)*], | |
adapter: { $($adapter)* }, | |
key: $key | |
}) | |
}; | |
// Munch a value and trailing comma from the token stream | |
(@ expect_value { | |
stream: [: $value:expr , $($stream:tt)*], | |
adapter: { $($adapter:tt)* }, | |
key: $key:ident | |
}) => { | |
__properties_internal!(@ with_adapter { | |
stream: [$($stream)*], | |
adapter: { $($adapter)* }, | |
key: $key, | |
value: $value | |
}) | |
}; | |
// Munch a trailing comma from the token stream | |
// The value is the key identifier as an expression | |
(@ expect_value { | |
stream: [, $($stream:tt)*], | |
adapter: { $($adapter:tt)* }, | |
key: $key:ident | |
}) => { | |
__properties_internal!(@ with_adapter { | |
stream: [$($stream)*], | |
adapter: { $($adapter)* }, | |
key: $key, | |
value: $key | |
}) | |
}; | |
// Munch a value from the end of the token stream | |
(@ expect_value { | |
stream: [: $value:expr], | |
adapter: { $($adapter:tt)* }, | |
key: $key:ident | |
}) => { | |
__properties_internal!(@ with_adapter { | |
stream: [], | |
adapter: { $($adapter)* }, | |
key: $key, | |
value: $value | |
}) | |
}; | |
// We've reached the end of the token stream | |
// The value is the key identifier as an expression | |
(@ expect_value { | |
stream: [], | |
adapter: { $($adapter:tt)* }, | |
key: $key:ident | |
}) => { | |
__properties_internal!(@ with_adapter { | |
stream: [], | |
adapter: { $($adapter)* }, | |
key: $key, | |
value: $key | |
}) | |
}; | |
// Use the adapter and replace with the default (no-op) | |
// The adapter is a function like `T -> impl ToValue` | |
(@ with_adapter { | |
stream: [$($stream:tt)*], | |
adapter: { | |
kind: $adapter_kind:ident | |
}, | |
key: $key:ident, | |
value: $value:expr | |
}) => { | |
__properties_internal!(@ with_value { | |
stream: [$($stream)*], | |
adapter_fn: $crate::adapter::map::$adapter_kind, | |
key: $key, | |
value: $value | |
}) | |
}; | |
// Use the adapter and replace with the default (no-op) | |
// The adapter is a function like `(T, F: impl Fn(&T) -> fmt::Result) -> impl ToValue` | |
(@ with_adapter { | |
stream: [$($stream:tt)*], | |
adapter: { | |
kind: $adapter_kind:ident, | |
state: $adapter_state:expr | |
}, | |
key: $key:ident, | |
value: $value:expr | |
}) => { | |
__properties_internal!(@ with_value { | |
stream: [$($stream)*], | |
adapter_fn: |value| $crate::adapter::map_with::$adapter_kind(value, $adapter_state), | |
key: $key, | |
value: $value | |
}) | |
}; | |
// Use the value with no adapter | |
// In this example we just print it | |
(@ with_value { | |
stream: [$($stream:tt)*], | |
adapter_fn: $adapter_fn:expr, | |
key: $key:ident, | |
value: $value:expr | |
}) => { | |
let value = &$value; | |
let adapter = $adapter_fn(value); | |
let value = serde_json::to_string(&adapter.to_value()).expect("failed to serialize"); | |
println!("{}: {}", stringify!($key), value); | |
__properties_internal!(@ expect_adapter { | |
stream: [$($stream)*] | |
}) | |
}; | |
); | |
use std::fmt::{Debug, Display}; | |
/// A single property value. | |
/// | |
/// Values implement `serde::Serialize`. | |
pub struct Value<'a> { | |
inner: ValueInner<'a>, | |
} | |
#[derive(Clone, Copy)] | |
enum ValueInner<'a> { | |
Fmt(&'a dyn Display), | |
#[cfg(feature = "erased-serde")] | |
Serde(&'a dyn erased_serde::Serialize), | |
} | |
impl<'a> serde::Serialize for Value<'a> { | |
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | |
where | |
S: serde::Serializer, | |
{ | |
match self.inner { | |
ValueInner::Fmt(v) => serializer.collect_str(v), | |
#[cfg(feature = "erased-serde")] | |
ValueInner::Serde(v) => v.serialize(serializer), | |
} | |
} | |
} | |
impl<'a> Value<'a> { | |
pub fn fmt(v: &'a impl Display) -> Self { | |
Value { | |
inner: ValueInner::Fmt(v), | |
} | |
} | |
#[cfg(feature = "erased-serde")] | |
pub fn serde(v: &'a impl serde::Serialize) -> Self { | |
Value { | |
inner: ValueInner::Serde(v), | |
} | |
} | |
} | |
pub trait ToValue { | |
fn to_value(&self) -> Value; | |
} | |
impl<'a> ToValue for Value<'a> { | |
fn to_value(&self) -> Value { | |
Value { inner: self.inner } | |
} | |
} | |
pub mod adapter { | |
use super::*; | |
pub mod map { | |
use serde; | |
use std::fmt::{Debug, Display}; | |
use super::*; | |
/// The default property adapter used when no `#[log]` attribute is present. | |
/// | |
/// If `std` is available, this will use `Serialize`. | |
/// If `std` is not available, this will use `Debug`. | |
pub fn default(v: impl serde::Serialize + Debug) -> impl ToValue { | |
#[cfg(feature = "erased-serde")] | |
{ | |
serde(v) | |
} | |
#[cfg(not(feature = "erased-serde"))] | |
{ | |
debug(v) | |
} | |
} | |
/// `#[log(serde)]` Format a property value using its `Serialize` implementation. | |
/// | |
/// The property value will retain its structure. | |
#[cfg(feature = "erased-serde")] | |
pub fn serde(v: impl serde::Serialize) -> impl ToValue { | |
struct SerdeAdapter<T>(T); | |
impl<T> ToValue for SerdeAdapter<T> | |
where | |
T: serde::Serialize, | |
{ | |
fn to_value(&self) -> Value { | |
Value::serde(&self.0) | |
} | |
} | |
SerdeAdapter(v) | |
} | |
/// `#[log(debug)]` Format a property value using its `Debug` implementation. | |
/// | |
/// The property value will be serialized as a string. | |
pub fn debug(v: impl Debug) -> impl ToValue { | |
map_with::fmt(v, Debug::fmt) | |
} | |
/// `#[log(display)]` Format a property value using its `Display` implementation. | |
/// | |
/// The property value will be serialized as a string. | |
pub fn display(v: impl Display) -> impl ToValue { | |
map_with::fmt(v, Display::fmt) | |
} | |
} | |
pub mod map_with { | |
use std::fmt::{Display, Formatter, Result}; | |
use super::*; | |
/// `#[log(fmt = expr)]` Format a property value using a specific format. | |
pub fn fmt<T>(value: T, adapter: impl Fn(&T, &mut Formatter) -> Result) -> impl ToValue { | |
struct FmtAdapter<T, F> { | |
value: T, | |
adapter: F, | |
} | |
impl<T, F> Display for FmtAdapter<T, F> | |
where | |
F: Fn(&T, &mut Formatter) -> Result, | |
{ | |
fn fmt(&self, f: &mut Formatter) -> Result { | |
(self.adapter)(&self.value, f) | |
} | |
} | |
impl<T, F> ToValue for FmtAdapter<T, F> | |
where | |
F: Fn(&T, &mut Formatter) -> Result, | |
{ | |
fn to_value(&self) -> Value { | |
Value::fmt(self) | |
} | |
} | |
FmtAdapter { value, adapter } | |
} | |
/// `#[log(with = expr)]` Use a generic adapter. | |
pub fn with<T, U>(value: T, adapter: impl Fn(T) -> U) -> U | |
where | |
U: ToValue, | |
{ | |
adapter(value) | |
} | |
} | |
} | |
fn main() { | |
let value2 = "a string"; | |
let some_number = || 4.03f32; | |
let some_err = || io::Error::from(io::ErrorKind::AlreadyExists); | |
#[derive(Debug)] | |
struct NotCopy; | |
let not_copy = NotCopy; | |
properties!({ | |
key1: 1, | |
#[log(fmt = Debug::fmt)] | |
key2: value2, | |
key3: some_number(), | |
key4: "another string", | |
#[log(debug)] | |
key5: not_copy, | |
#[log(with = error)] | |
err1: some_err(), | |
}); | |
drop(not_copy); | |
} | |
use std::{error::Error, io}; | |
// An example generic adapter | |
// This adapter formats errors as strings | |
fn error<'a, E>(err: &'a E) -> impl ToValue + 'a | |
where | |
E: Error + 'a, | |
{ | |
use std::fmt; | |
struct ErrorAdapter<'a, E: 'a>(&'a E); | |
impl<'a, E> fmt::Display for ErrorAdapter<'a, E> | |
where | |
E: Error + 'a, | |
{ | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
writeln!(f, "ERROR!!! {}", self.0.description())?; | |
let mut current_cause = self.0.cause(); | |
while let Some(cause) = current_cause { | |
writeln!(f, " caused by: {}", cause.description())?; | |
current_cause = cause.cause(); | |
} | |
Ok(()) | |
} | |
} | |
impl<'a, E> ToValue for ErrorAdapter<'a, E> | |
where | |
E: Error + 'a, | |
{ | |
fn to_value(&self) -> Value { | |
Value::fmt(self) | |
} | |
} | |
ErrorAdapter(err) | |
} | |
// Another example generic adapter | |
// This adapter formats errors as nested datastructures | |
#[cfg(feature = "erased-serde")] | |
fn error_serde<'a, E>(err: &'a E) -> impl ToValue + 'a | |
where | |
E: Error + 'a | |
{ | |
use serde::ser::{Serializer, SerializeMap}; | |
struct ErrorAdapter<'a, E: ?Sized + 'a>(&'a E); | |
impl<'a, E> serde::Serialize for ErrorAdapter<'a, E> | |
where | |
E: ?Sized + Error + 'a | |
{ | |
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | |
where | |
S: Serializer, | |
{ | |
let mut map = serializer.serialize_map(None)?; | |
map.serialize_entry("description", self.0.description())?; | |
if let Some(cause) = self.0.cause() { | |
map.serialize_entry("cause", &ErrorAdapter(cause))?; | |
} | |
map.end() | |
} | |
} | |
impl<'a, E> ToValue for ErrorAdapter<'a, E> | |
where | |
E: Error + 'a, | |
{ | |
fn to_value(&self) -> Value { | |
Value::serde(self) | |
} | |
} | |
ErrorAdapter(err) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment