Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,12 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"

[[package]]
name = "glob"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"

[[package]]
name = "globset"
version = "0.4.18"
Expand Down Expand Up @@ -2505,6 +2511,12 @@ dependencies = [
"spin",
]

[[package]]
name = "leb128"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52"

[[package]]
name = "libbz2-rs-sys"
version = "0.2.5"
Expand Down Expand Up @@ -4986,8 +4998,20 @@ dependencies = [
name = "relay-serialization"
version = "26.7.2"
dependencies = [
"leb128",
"prost 0.14.4",
"relay-serialization-derive",
"serde",
"serde_json",
"trybuild",
]

[[package]]
name = "relay-serialization-derive"
version = "26.7.2"
dependencies = [
"quote",
"syn",
]

[[package]]
Expand Down Expand Up @@ -5060,6 +5084,7 @@ dependencies = [
"relay-redis",
"relay-replays",
"relay-sampling",
"relay-serialization",
"relay-spans",
"relay-statsd",
"relay-system",
Expand Down Expand Up @@ -5794,6 +5819,15 @@ dependencies = [
"serde_core",
]

[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]

[[package]]
name = "serde_test"
version = "1.0.177"
Expand Down Expand Up @@ -6369,6 +6403,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"

[[package]]
name = "target-triple"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171"

[[package]]
name = "tempfile"
version = "3.27.0"
Expand All @@ -6382,6 +6422,15 @@ dependencies = [
"windows-sys 0.61.2",
]

[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]

[[package]]
name = "terminal_size"
version = "0.4.4"
Expand Down Expand Up @@ -6577,6 +6626,21 @@ dependencies = [
"tokio",
]

[[package]]
name = "toml"
version = "1.1.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]

[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
Expand All @@ -6600,13 +6664,19 @@ dependencies = [

[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]

[[package]]
name = "toml_writer"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"

[[package]]
name = "tonic"
version = "0.14.6"
Expand Down Expand Up @@ -6784,6 +6854,21 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"

[[package]]
name = "trybuild"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4"
dependencies = [
"glob",
"serde",
"serde_derive",
"serde_json",
"target-triple",
"termcolor",
"toml",
]

[[package]]
name = "twox-hash"
version = "2.1.2"
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ relay-redis = { path = "relay-redis" }
relay-replays = { path = "relay-replays" }
relay-sampling = { path = "relay-sampling" }
relay-serialization = { path = "relay-serialization" }
relay-serialization-derive = { path = "relay-serialization-derive" }
relay-server = { path = "relay-server" }
relay-spans = { path = "relay-spans" }
relay-statsd = { path = "relay-statsd" }
Expand Down Expand Up @@ -145,6 +146,7 @@ insta = { version = "1", features = ["json", "redactions", "ron"] }
ipnetwork = "0.21"
itertools = "0.14"
json-forensics = "0.1"
leb128 = "0.2.7"
libc = "0.2"
liblzma = "0.4"
lru = "0.16"
Expand Down Expand Up @@ -238,6 +240,7 @@ tower = { version = "0.5", default-features = false }
tower-http = { version = "0.6", default-features = false }
tracing = "0.1"
tracing-subscriber = "0.3"
trybuild = "1.0.120"
uaparser = "0.6"
unescaper = "0.1"
unicase = "2"
Expand Down
20 changes: 20 additions & 0 deletions relay-serialization-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "relay-serialization-derive"
authors = ["Sentry <oss@sentry.io>"]
description = "Derive macros for relay serialization tools"
homepage = "https://getsentry.github.io/relay/"
repository = "https://github.com/getsentry/relay"
version = "26.7.2"
edition = "2024"
license-file = "../LICENSE.md"
publish = false

[lints]
workspace = true

[dependencies]
quote = { workspace = true }
syn = { workspace = true }

[lib]
proc-macro = true
177 changes: 177 additions & 0 deletions relay-serialization-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//! A macro to add deserialization bounds to a prost message.
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{DeriveInput, LitStr, PathArguments, Type, parse_macro_input};

#[proc_macro_derive(RuntimeDescription)]
pub fn derive(s: TokenStream) -> TokenStream {
let input = parse_macro_input!(s as DeriveInput);

let mut nested_v = vec![];

match input.data {
syn::Data::Struct(data_struct) => {
if let syn::Fields::Named(fields_named) = data_struct.fields {
for field in fields_named.named.iter() {
for attr in &field.attrs {
if attr.path().is_ident("prost")
&& let Err(value) = collect_tags_and_types(&mut nested_v, field, attr)
{
return value;
}
}
}
}
}
syn::Data::Enum(data_enum) => {
for variant in data_enum.variants.iter() {
for attr in &variant.attrs {
if attr.path().is_ident("prost")
&& let Some(first_field) = variant.fields.iter().next()
&& let Err(value) = collect_tags_and_types(&mut nested_v, first_field, attr)
{
return value;
}
}
}
}
syn::Data::Union(_) => {
let span = input.span();
return quote_spanned! {
span => compile_error!("unions are unsupported")
}
.into();
}
}

let qs = nested_v.into_iter().map(|(typ, tag)| {
if let TypeKind::Field(tag) = tag {
quote! {
::relay_serialization::prost::Nested::Field(#tag, < #typ as ::relay_serialization::prost::RuntimeDescription>::desc)
}
} else {
quote! {
::relay_serialization::prost::Nested::Oneof(< #typ as ::relay_serialization::prost::RuntimeDescription>::desc)
}
}
});

let typ = &input.ident;
quote! {
impl ::relay_serialization::prost::RuntimeDescription for #typ {
fn desc() -> &'static [::relay_serialization::prost::Nested] {
&[#(#qs,)*]
}
}
}
.into()
}

enum TypeKind {
Field(u32),
OneOf,
}

fn collect_tags_and_types(
tags_and_types: &mut Vec<(Type, TypeKind)>,
field: &syn::Field,
attr: &syn::Attribute,
) -> Result<(), TokenStream> {
let mut tag: Option<TypeKind> = None;
let mut message_type: Option<&Type> = None;

let result = attr.parse_nested_meta(|meta| {
// Fun landmine: parse_nested_meta assumes you consume all the parsed--for tags like
// "#[foo(bar)]", bar is consumed automatically just before this callback is called,
// but "#[foo(bar = 6)]", the "= 6" value is not, and you MUST consume that in this callback
// or the parser will try to parse the "next" value in the stream, but the stream
// won't have advanced correctly, leading to sadness.
if meta.path.is_ident("tag") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
let tag_val = s
.value()
.parse::<u32>()
.map_err(|_| meta.input.error("error parsing tag"))?;

tag = TypeKind::Field(tag_val).into();
return Ok(());
}

if meta.path.is_ident("map")
|| meta.path.is_ident("btree_map")
|| meta.path.is_ident("hash_map")
{
return Err(meta.input.error("map types are currently unsupported"));
}

if meta.path.is_ident("group") {
return Err(meta.input.error("group types are unsupported"));
}

if meta.path.is_ident("message") {
message_type = innermost_contained_type(&field.ty);
return Ok(());
}

if meta.path.is_ident("oneof") {
message_type = innermost_contained_type(&field.ty);
let value = meta.value()?;
let _: LitStr = value.parse()?; // Consumes the tags

// Tag = None communicates that this is a one-of.
tag = Some(TypeKind::OneOf);
return Ok(());
}

// Things we don't care about, but are more than just a tag
if meta.path.is_ident("bytes")
|| meta.path.is_ident("enumeration")
|| meta.path.is_ident("tags")
|| meta.path.is_ident("packed")
{
let value = meta.value()?;
let _: LitStr = value.parse()?;
return Ok(());
}

// By default, just skip everything else; those should be just tags (and if not, we'll get
// an error, albeit a confusing one.)
Ok(())
Comment thread
cursor[bot] marked this conversation as resolved.
});

if let Err(e) = result {
return Err(e.to_compile_error().into());
}

if let Some(typ) = message_type {
let Some(tag) = tag else {
let span = attr.span();
return Err(quote_spanned! {
span => compile_error!("missing tag value");
}
.into());
};
tags_and_types.push((typ.to_owned(), tag));
}
Ok(())
}

fn innermost_contained_type(typ: &Type) -> Option<&Type> {
let Type::Path(p) = typ else {
return None;
};

let last_part = p.path.segments.last()?;

if let PathArguments::AngleBracketed(inner) = &last_part.arguments {
let first = inner.args.first()?;
match first {
syn::GenericArgument::Type(t) => innermost_contained_type(t),
_ => None,
}
} else {
Some(typ)
}
}
Loading
Loading