Skip to content

Instantly share code, notes, and snippets.

@KodrAus
Last active June 11, 2018 07:51
Show Gist options
  • Save KodrAus/b88fed867f4028e0deadc06c8966d51a to your computer and use it in GitHub Desktop.
Save KodrAus/b88fed867f4028e0deadc06c8966d51a to your computer and use it in GitHub Desktop.
Properties Macro
/*!
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