backbeat_macros/lib.rs
1// Copyright (c) 2026 Cameron Bytheway
2// SPDX-License-Identifier: MIT
3
4//! `#[derive(Event)]` for backbeat.
5//!
6//! Annotate a `#[repr(C)]` struct to make it a traceable event. The derive reflects the struct's
7//! fields into a `const` [`EventSchema`] (offsets via [`core::mem::offset_of!`], widths via
8//! [`core::mem::size_of`]), computes a content-addressed [`EventId`] by hashing that whole schema,
9//! and implements [`backbeat::Event`] — whose `zerocopy::IntoBytes` bound makes recording a single
10//! memcpy and rejects padded layouts the reader can't describe.
11//!
12//! ```ignore
13//! use backbeat::{Event, EventEnum};
14//! use backbeat::zerocopy::{Immutable, IntoBytes};
15//!
16//! /// Which way a frame is going.
17//! #[derive(EventEnum, IntoBytes, Immutable, Clone, Copy)]
18//! #[repr(u8)]
19//! enum Direction { Incoming = 0, Outgoing = 1 }
20//!
21//! /// A frame was queued for sending.
22//! #[derive(Event, IntoBytes, Immutable)]
23//! #[event(namespace = "my_crate::frame")]
24//! #[repr(C)]
25//! struct QueueData {
26//! #[event(key)] packet_number: u64,
27//! /// Offset into the stream.
28//! #[event(unit = "bytes")] offset: u64,
29//! direction: Direction, // a strongly-typed enum field
30//! is_fin: bool,
31//! }
32//! ```
33//!
34//! The generated code exposes `QueueData::SCHEMA` (an `EventSchema`), `QueueData::ID` (its
35//! `EventId`), and `QueueData::QUALIFIED_NAME`, and implements `Event`. Because the id hashes the
36//! whole schema, two builds whose layout or field metadata differ get distinct ids and never alias
37//! in a dump's registry.
38//!
39//! Container attributes (on the struct):
40//!
41//! * `#[event(namespace = "…")]` — required; the event's namespace prefix.
42//! * `#[event(span = enter)]` / `#[event(span = exit)]` — mark this event as one half of a span, so
43//! the trace converter can pair begin/end records into a duration slice. A spanned event must
44//! carry exactly one `#[event(span_id)]` field.
45//!
46//! Field attributes (mutually-exclusive *roles* — at most one per field):
47//!
48//! * `#[event(key)]` — promote this field to a top-level join/index column in the output table.
49//! * `#[event(span_id)]` — this `u64` is the span's own id (required on a `span = enter|exit`
50//! event; the enter and exit halves carry the same value so the converter pairs them).
51//! * `#[event(parent_span_id)]` — this `u64` is the enclosing span's id, linking this event under
52//! its parent. Allowed on any event, including plain (non-span) ones.
53//!
54//! Other field attributes (combine with a role):
55//!
56//! * `#[event(unit = "…")]` — attach a unit hint (`"bytes"`, `"ns"`, …) carried into the output.
57//! * `#[event(sentinel = <const>)]` — declare an in-band "absent" marker for an integer field (e.g.
58//! `sentinel = u64::MAX` for an unassigned packet number, or `sentinel = 0` for a `dump_id` only
59//! some events carry). The converter maps a field equal to its sentinel to SQL NULL, so the wire
60//! format needs no nulls yet `WHERE x IS NULL` works.
61//! * `#[event(interned)]` / `#[event(interned(dynamic))]` — the (`u32`) field is an intern id
62//! resolved against the dump's intern table; `dynamic` marks runtime-built values.
63//!
64//! Enum-typed fields use a separate `#[derive(EventEnum)]` on the (`#[repr(uN)]`, fieldless) enum;
65//! the field then carries the strong type and the schema records its value→label map automatically.
66//!
67//! Field and struct doc comments (`///`) are lifted verbatim into the schema's `description`
68//! fields, so the embedded registry documents itself.
69
70use proc_macro::TokenStream;
71use proc_macro2::TokenStream as TokenStream2;
72use quote::quote;
73use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, LitStr, Type};
74
75/// A field's role, mirroring `backbeat::schema::FieldRole` on the macro side.
76#[derive(Clone, Copy, PartialEq, Eq)]
77enum Role {
78 Key,
79 SpanId,
80 ParentSpanId,
81}
82
83impl Role {
84 /// The `backbeat::schema::FieldRole` variant tokens this role emits.
85 fn tokens(self) -> TokenStream2 {
86 match self {
87 Role::Key => quote! { ::backbeat::schema::FieldRole::Key },
88 Role::SpanId => quote! { ::backbeat::schema::FieldRole::SpanId },
89 Role::ParentSpanId => quote! { ::backbeat::schema::FieldRole::ParentSpanId },
90 }
91 }
92}
93
94/// A struct's span phase, mirroring `backbeat::schema::Phase`.
95#[derive(Clone, Copy, PartialEq, Eq)]
96enum Phase {
97 None,
98 Enter,
99 Exit,
100}
101
102impl Phase {
103 fn tokens(self) -> TokenStream2 {
104 match self {
105 Phase::None => quote! { ::backbeat::schema::Phase::None },
106 Phase::Enter => quote! { ::backbeat::schema::Phase::Enter },
107 Phase::Exit => quote! { ::backbeat::schema::Phase::Exit },
108 }
109 }
110}
111
112/// Derives `Event` for a struct: its compile-time `EventId`, a reflected `EventSchema`, and an
113/// `impl Event`.
114#[proc_macro_derive(Event, attributes(event))]
115pub fn derive_event(input: TokenStream) -> TokenStream {
116 let input = parse_macro_input!(input as DeriveInput);
117 match expand(input) {
118 Ok(ts) => ts.into(),
119 Err(e) => e.to_compile_error().into(),
120 }
121}
122
123/// Derives `EventEnum` for a fieldless `#[repr(u8|u16|u32|u64)]` enum, so it can be a strongly-typed
124/// event field. Emits the variant→label map and the repr width.
125#[proc_macro_derive(EventEnum)]
126pub fn derive_event_enum(input: TokenStream) -> TokenStream {
127 let input = parse_macro_input!(input as DeriveInput);
128 match expand_event_enum(input) {
129 Ok(ts) => ts.into(),
130 Err(e) => e.to_compile_error().into(),
131 }
132}
133
134fn expand_event_enum(input: DeriveInput) -> syn::Result<TokenStream2> {
135 let name = &input.ident;
136
137 let data = match &input.data {
138 Data::Enum(e) => e,
139 _ => {
140 return Err(syn::Error::new_spanned(
141 &input,
142 "`#[derive(EventEnum)]` can only be applied to an enum",
143 ))
144 }
145 };
146
147 // The discriminant repr width must be an explicit `#[repr(u8|u16|u32|u64)]` — the recorder
148 // stores the enum inline at that width, and the reader needs it to read the discriminant.
149 let repr = enum_repr_width(&input)?;
150
151 // Each variant must be fieldless (it sits inline as a bare discriminant) and contributes a
152 // value→label pair. An explicit discriminant sets the value; otherwise it follows C rules
153 // (previous + 1, starting at 0). We require explicit discriminants so the on-disk value is
154 // never silently shifted by reordering — the value is part of the event's identity.
155 let mut labels = Vec::with_capacity(data.variants.len());
156 for variant in &data.variants {
157 if !matches!(variant.fields, Fields::Unit) {
158 return Err(syn::Error::new_spanned(
159 variant,
160 "`#[derive(EventEnum)]` requires fieldless variants",
161 ));
162 }
163 let label = variant.ident.to_string();
164 let value =
165 match &variant.discriminant {
166 Some((_, expr)) => expr.clone(),
167 None => return Err(syn::Error::new_spanned(
168 variant,
169 "`#[derive(EventEnum)]` requires an explicit discriminant, e.g. `Variant = 0` \
170 (the value is part of the event's on-disk identity)",
171 )),
172 };
173 labels.push(quote! {
174 ::backbeat::schema::EnumLabel { value: (#value) as u64, label: #label }
175 });
176 }
177
178 Ok(quote! {
179 impl ::backbeat::EventEnum for #name {
180 const REPR: u8 = #repr;
181 const LABELS: &'static [::backbeat::schema::EnumLabel] = &[ #(#labels),* ];
182 }
183 })
184}
185
186/// Reads the discriminant byte-width from a `#[repr(uN)]` attribute on an enum.
187fn enum_repr_width(input: &DeriveInput) -> syn::Result<u8> {
188 for attr in &input.attrs {
189 if !attr.path().is_ident("repr") {
190 continue;
191 }
192 let mut width = None;
193 attr.parse_nested_meta(|meta| {
194 width = meta
195 .path
196 .get_ident()
197 .and_then(|i| match i.to_string().as_str() {
198 "u8" => Some(1),
199 "u16" => Some(2),
200 "u32" => Some(4),
201 "u64" => Some(8),
202 _ => None,
203 });
204 Ok(())
205 })?;
206 if let Some(w) = width {
207 return Ok(w);
208 }
209 }
210 Err(syn::Error::new_spanned(
211 input,
212 "`#[derive(EventEnum)]` requires an explicit `#[repr(u8|u16|u32|u64)]`",
213 ))
214}
215
216fn expand(input: DeriveInput) -> syn::Result<TokenStream2> {
217 let name = &input.ident;
218
219 // Container attributes: namespace (required) and span phase (optional).
220 let container = ContainerAttrs::parse(&input)?;
221
222 // `#[derive(Event)]` describes a fixed C layout; anything else has no stable offsets to reflect.
223 let fields =
224 match &input.data {
225 Data::Struct(s) => match &s.fields {
226 Fields::Named(named) => named.named.iter().collect::<Vec<_>>(),
227 // No fields is legal only for a plain marker event — a span needs a `span_id` field.
228 Fields::Unit => Vec::new(),
229 Fields::Unnamed(_) => return Err(syn::Error::new_spanned(
230 &s.fields,
231 "`#[derive(Event)]` requires named fields (tuple structs have no field names \
232 to use as column names)",
233 )),
234 },
235 _ => {
236 return Err(syn::Error::new_spanned(
237 &input,
238 "`#[derive(Event)]` can only be applied to a struct",
239 ))
240 }
241 };
242
243 let description = doc_of(&input.attrs);
244
245 let mut field_defs = Vec::with_capacity(fields.len());
246 // Track span-role fields for cross-field validation after the loop.
247 let mut span_id_fields = 0usize;
248 let mut parent_span_fields = 0usize;
249 for field in &fields {
250 let ident = field.ident.as_ref().expect("named field");
251 let fname = ident.to_string();
252 let fdesc = doc_of(&field.attrs);
253 let attrs = FieldAttrs::parse(&field.attrs)?;
254
255 // A span-id / parent-span-id field must be a bare `u64`: the converter compares ids for
256 // equality across the enter and exit structs, so a uniform width avoids cross-struct
257 // mismatch, and these roles are meaningless on non-integer/narrower types.
258 if matches!(attrs.role, Some(Role::SpanId | Role::ParentSpanId)) {
259 require_u64(&field.ty, attrs.role.unwrap())?;
260 }
261
262 // `sentinel` is an integer "absent" marker, mapped to NULL by comparing the raw integer
263 // image. It is meaningless on a `bool`, a byte array, or an interned (string) field — reject
264 // the cases the macro can see syntactically so a mistake is a clear compile error rather
265 // than silently nulling valid rows (e.g. a `sentinel = 1` on a `bool` nulling every `true`).
266 if attrs.sentinel.is_some() {
267 if attrs.interned.is_some() {
268 return Err(syn::Error::new_spanned(
269 field,
270 "`#[event(sentinel = …)]` is not valid on an interned field (it marks an \
271 absent *integer*, compared against the raw value)",
272 ));
273 }
274 if is_bool(&field.ty) || is_byte_array(&field.ty) {
275 return Err(syn::Error::new_spanned(
276 &field.ty,
277 "`#[event(sentinel = …)]` is only valid on an integer field (the sentinel is \
278 an in-band absent marker compared against the raw integer value)",
279 ));
280 }
281 }
282 match attrs.role {
283 Some(Role::SpanId) => span_id_fields += 1,
284 Some(Role::ParentSpanId) => parent_span_fields += 1,
285 _ => {}
286 }
287
288 let fty = &field.ty;
289
290 // Resolve the field's `FieldType` and labels. An `#[event(interned)]` field is a `u32`
291 // intern id (attribute-driven). Everything else goes through the `FieldTy` trait, resolved
292 // at const-eval: primitives, `[u8; N]`, and any `#[derive(EventEnum)]` type all implement
293 // it, so the macro doesn't need to know the field's concrete type — including enums, whose
294 // variants it cannot see.
295 let (ty_expr, labels_expr) = if let Some(dynamic) = attrs.interned {
296 (
297 quote! { ::backbeat::schema::FieldType::Interned { dynamic: #dynamic } },
298 quote! { &[] },
299 )
300 } else {
301 (
302 quote! { <#fty as ::backbeat::FieldTy>::FIELD_TYPE },
303 quote! { <#fty as ::backbeat::FieldTy>::LABELS },
304 )
305 };
306
307 let desc_expr = opt_str(fdesc.as_deref());
308 let unit_expr = opt_str(attrs.unit.as_deref());
309 let role_expr = match attrs.role {
310 Some(r) => r.tokens(),
311 None => quote! { ::backbeat::schema::FieldRole::None },
312 };
313 // Emit the sentinel as the zero-extended image of the field's stored bytes, which is what
314 // the converter reconstructs with a width-bounded `read_uint`. We (a) cast through the
315 // field's own type so the literal is range-checked and signed values are well-defined, then
316 // (b) widen to `u64` and mask to the field width to STRIP the sign extension that an
317 // `iN as u64` performs. So `#[event(sentinel = -1)] code: i32` yields
318 // `((-1i32) as u64) & 0xFFFF_FFFF` = `0x0000_0000_FFFF_FFFF` — the zero-extension of the 4
319 // on-disk bytes. A full-width `u64::MAX`/`0` masks with `u64::MAX` (the `>= 8` guard avoids
320 // a `1 << 64` overflow). All in `const` context.
321 let sentinel_expr = match &attrs.sentinel {
322 Some(expr) => quote! {
323 ::core::option::Option::Some({
324 let w = ::core::mem::size_of::<#fty>();
325 let mask = if w >= 8 { u64::MAX } else { (1u64 << (8 * w)) - 1 };
326 (((#expr) as #fty) as u64) & mask
327 })
328 },
329 None => quote! { ::core::option::Option::None },
330 };
331
332 field_defs.push(quote! {
333 ::backbeat::schema::FieldSchema {
334 name: #fname,
335 description: #desc_expr,
336 ty: #ty_expr,
337 offset: ::backbeat::schema::layout_u16(::core::mem::offset_of!(#name, #ident)),
338 width: ::backbeat::schema::layout_u16(::core::mem::size_of::<#fty>()),
339 role: #role_expr,
340 unit: #unit_expr,
341 sentinel: #sentinel_expr,
342 enum_labels: #labels_expr,
343 }
344 });
345 }
346
347 // Cross-field span validation.
348 if span_id_fields > 1 {
349 return Err(syn::Error::new_spanned(
350 &input,
351 "an event may declare at most one `#[event(span_id)]` field",
352 ));
353 }
354 if parent_span_fields > 1 {
355 return Err(syn::Error::new_spanned(
356 &input,
357 "an event may declare at most one `#[event(parent_span_id)]` field",
358 ));
359 }
360 match container.phase {
361 // A spanned event must carry exactly one span id (the value enter/exit are paired by).
362 Phase::Enter | Phase::Exit if span_id_fields == 0 => {
363 return Err(syn::Error::new_spanned(
364 &input,
365 "`#[event(span = enter|exit)]` requires exactly one `#[event(span_id)]` field",
366 ));
367 }
368 // A span id only has meaning on an enter/exit event; a plain event associates with a span
369 // via `parent_span_id` instead.
370 Phase::None if span_id_fields > 0 => {
371 return Err(syn::Error::new_spanned(
372 &input,
373 "`#[event(span_id)]` requires the event to be a span \
374 (`#[event(span = enter)]` or `#[event(span = exit)]`)",
375 ));
376 }
377 _ => {}
378 }
379
380 Ok(emit(name, &container, description, field_defs))
381}
382
383/// Emits the inherent consts (`ID`, `QUALIFIED_NAME`, `SCHEMA`) and the `Event` impl.
384fn emit(
385 name: &syn::Ident,
386 container: &ContainerAttrs,
387 description: Option<String>,
388 field_defs: Vec<TokenStream2>,
389) -> TokenStream2 {
390 let qualified = format!("{}::{name}", container.namespace);
391 let desc_expr = opt_str(description.as_deref());
392 let phase_expr = container.phase.tokens();
393
394 quote! {
395 impl #name {
396 /// Fully-qualified event name, `"namespace::TypeName"`.
397 pub const QUALIFIED_NAME: &'static str = #qualified;
398
399 /// The reflected field layout (also held by [`Self::SCHEMA`]). Named so [`Self::ID`] can
400 /// hash it without referencing `SCHEMA` (which embeds the id — that would be circular).
401 const FIELDS: &'static [::backbeat::schema::FieldSchema] = &[ #(#field_defs),* ];
402
403 /// Content-addressed event id: a hash of the whole schema (name, phase, every field's
404 /// name/type/offset/width/role/unit and any enum labels). Two builds with differing
405 /// layouts get distinct ids and are treated as separate event types sharing a name.
406 pub const ID: ::backbeat::id::EventId =
407 ::backbeat::schema::EventSchema::compute_id(
408 Self::QUALIFIED_NAME,
409 #phase_expr,
410 Self::FIELDS,
411 );
412
413 /// Self-describing layout of this event, reflected from its fields at compile time.
414 pub const SCHEMA: ::backbeat::schema::EventSchema =
415 ::backbeat::schema::EventSchema {
416 id: Self::ID,
417 qualified_name: Self::QUALIFIED_NAME,
418 description: #desc_expr,
419 record_size: ::backbeat::schema::layout_u16(::core::mem::size_of::<#name>()),
420 phase: #phase_expr,
421 fields: Self::FIELDS,
422 };
423 }
424
425 impl ::backbeat::Event for #name {
426 const SCHEMA: ::backbeat::schema::EventSchema = Self::SCHEMA;
427 const ID: ::backbeat::id::EventId = Self::ID;
428 const QUALIFIED_NAME: &'static str = Self::QUALIFIED_NAME;
429 }
430
431 // Register the type so the dumper can self-populate its schema registry. Expands to a
432 // `submit!` under `std` and to nothing on `no_std` (see `backbeat::register_event!`).
433 ::backbeat::register_event!(#name);
434 }
435}
436
437/// Field-level `#[event(...)]` attributes.
438#[derive(Default)]
439struct FieldAttrs {
440 /// The field's role, if any (`key`/`span_id`/`parent_span_id`). At most one — a second is an
441 /// error, so the illegal combinations are unrepresentable.
442 role: Option<Role>,
443 unit: Option<String>,
444 /// `Some(dynamic)` if the field is interned.
445 interned: Option<bool>,
446 /// `Some(expr)` if the field declares a `#[event(sentinel = …)]` "absent" marker. The expression
447 /// is emitted as `(expr) as <field type> as u64` so a typed/signed/narrow constant (`u64::MAX`,
448 /// `0`, `-1` on an `i32`, a named const) widens to the same image the converter reads back.
449 sentinel: Option<Expr>,
450}
451
452impl FieldAttrs {
453 fn parse(attrs: &[syn::Attribute]) -> syn::Result<Self> {
454 let mut out = FieldAttrs::default();
455 for attr in attrs {
456 if !attr.path().is_ident("event") {
457 continue;
458 }
459 attr.parse_nested_meta(|meta| {
460 if meta.path.is_ident("key") {
461 out.set_role(Role::Key, &meta)?;
462 Ok(())
463 } else if meta.path.is_ident("span_id") {
464 out.set_role(Role::SpanId, &meta)?;
465 Ok(())
466 } else if meta.path.is_ident("parent_span_id") {
467 out.set_role(Role::ParentSpanId, &meta)?;
468 Ok(())
469 } else if meta.path.is_ident("unit") {
470 let lit: LitStr = meta.value()?.parse()?;
471 out.unit = Some(lit.value());
472 Ok(())
473 } else if meta.path.is_ident("sentinel") {
474 // `sentinel = <expr>`: an integer constant the producer uses to mean "absent".
475 let expr: Expr = meta.value()?.parse()?;
476 out.sentinel = Some(expr);
477 Ok(())
478 } else if meta.path.is_ident("interned") {
479 // `interned` or `interned(dynamic)`.
480 let mut dynamic = false;
481 if meta.input.peek(syn::token::Paren) {
482 meta.parse_nested_meta(|inner| {
483 if inner.path.is_ident("dynamic") {
484 dynamic = true;
485 Ok(())
486 } else {
487 Err(inner.error("expected `dynamic`"))
488 }
489 })?;
490 }
491 out.interned = Some(dynamic);
492 Ok(())
493 } else {
494 Err(meta.error("unknown `#[event(...)]` field attribute"))
495 }
496 })?;
497 }
498 Ok(out)
499 }
500
501 /// Sets the field's role, erroring if one was already set (roles are mutually exclusive).
502 fn set_role(&mut self, role: Role, meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
503 if self.role.is_some() {
504 return Err(meta.error(
505 "a field may have at most one role \
506 (`key` / `span_id` / `parent_span_id` are mutually exclusive)",
507 ));
508 }
509 self.role = Some(role);
510 Ok(())
511 }
512}
513
514/// Container-level (`#[event(...)]` on the struct) attributes: the required namespace and the
515/// optional span phase.
516struct ContainerAttrs {
517 namespace: String,
518 phase: Phase,
519}
520
521impl ContainerAttrs {
522 fn parse(input: &DeriveInput) -> syn::Result<Self> {
523 let mut namespace = None;
524 let mut phase = Phase::None;
525 for attr in &input.attrs {
526 if !attr.path().is_ident("event") {
527 continue;
528 }
529 attr.parse_nested_meta(|meta| {
530 if meta.path.is_ident("namespace") {
531 let lit: LitStr = meta.value()?.parse()?;
532 namespace = Some(lit.value());
533 Ok(())
534 } else if meta.path.is_ident("span") {
535 // `span = enter` or `span = exit`.
536 let ident: syn::Ident = meta.value()?.parse()?;
537 phase = match ident.to_string().as_str() {
538 "enter" => Phase::Enter,
539 "exit" => Phase::Exit,
540 _ => {
541 return Err(meta.error("`span` must be `enter` or `exit`"));
542 }
543 };
544 Ok(())
545 } else {
546 Err(meta.error("unknown container-level `#[event(...)]` attribute"))
547 }
548 })?;
549 }
550 let namespace = namespace.ok_or_else(|| {
551 syn::Error::new_spanned(
552 input,
553 "`#[derive(Event)]` requires `#[event(namespace = \"...\")]`",
554 )
555 })?;
556 Ok(Self { namespace, phase })
557 }
558}
559
560/// Whether a field type is the bare `bool` primitive.
561fn is_bool(ty: &Type) -> bool {
562 matches!(ty, Type::Path(p) if p.path.is_ident("bool"))
563}
564
565/// Whether a field type is an array `[_; N]` (e.g. `[u8; 16]` — a `Bytes` field).
566fn is_byte_array(ty: &Type) -> bool {
567 matches!(ty, Type::Array(_))
568}
569
570/// Enforces that a `span_id` / `parent_span_id` field is a bare `u64`.
571fn require_u64(ty: &Type, role: Role) -> syn::Result<()> {
572 let is_u64 = matches!(ty, Type::Path(p) if p.path.is_ident("u64"));
573 if is_u64 {
574 Ok(())
575 } else {
576 let attr = match role {
577 Role::SpanId => "span_id",
578 Role::ParentSpanId => "parent_span_id",
579 Role::Key => unreachable!("require_u64 is only called for span roles"),
580 };
581 Err(syn::Error::new_spanned(
582 ty,
583 format!(
584 "`#[event({attr})]` fields must be `u64` (span ids are compared for equality \
585 across the enter/exit events, so they need a uniform width)"
586 ),
587 ))
588 }
589}
590
591/// Joins the `///` doc-comment lines on `attrs` into a single trimmed string, or `None` if there
592/// are none. Each `///` line is a `#[doc = "..."]` attribute with a leading space.
593fn doc_of(attrs: &[syn::Attribute]) -> Option<String> {
594 let mut lines = Vec::new();
595 for attr in attrs {
596 if !attr.path().is_ident("doc") {
597 continue;
598 }
599 if let syn::Meta::NameValue(nv) = &attr.meta {
600 if let Expr::Lit(ExprLit {
601 lit: Lit::Str(s), ..
602 }) = &nv.value
603 {
604 lines.push(s.value().trim().to_string());
605 }
606 }
607 }
608 if lines.is_empty() {
609 None
610 } else {
611 Some(lines.join("\n"))
612 }
613}
614
615/// `Some("…")` → `::core::option::Option::Some("…")`, `None` → `::core::option::Option::None`.
616fn opt_str(s: Option<&str>) -> TokenStream2 {
617 match s {
618 Some(s) => quote! { ::core::option::Option::Some(#s) },
619 None => quote! { ::core::option::Option::None },
620 }
621}