Skip to main content

relay_event_derive/
lib.rs

1//! Derive for visitor traits on the Event schema.
2//!
3//! This derive provides the `ProcessValue` trait. It must be used within the `relay-event-schema`
4//! crate.
5
6#![warn(missing_docs)]
7#![doc(
8    html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png",
9    html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
10)]
11#![recursion_limit = "256"]
12
13use proc_macro2::{Span, TokenStream};
14use quote::{ToTokens, quote};
15use syn::parse::ParseStream;
16use syn::{ExprPath, Ident, Lit, LitBool, LitInt, LitStr};
17use synstructure::decl_derive;
18
19mod utils;
20
21use utils::SynstructureExt as _;
22
23decl_derive!(
24    [ProcessValue, attributes(metastructure)] =>
25    /// Derive the `ProcessValue` trait.
26    derive_process_value
27);
28
29fn derive_process_value(mut s: synstructure::Structure<'_>) -> syn::Result<TokenStream> {
30    let _ = s.bind_with(|_bi| synstructure::BindStyle::RefMut);
31    let _ = s.add_bounds(synstructure::AddBounds::Generics);
32
33    let type_attrs = parse_type_attributes(&s)?;
34    let process_func_call_tokens = type_attrs.process_func_call_tokens();
35
36    let process_value_arms = s.try_each_variant(|variant| {
37        if is_newtype(variant) {
38            // Process variant twice s.t. both processor functions are called.
39            //
40            // E.g.:
41            // - For `Value::String`, call `process_string` as well as `process_value`.
42            // - For `LenientString`, call `process_lenient_string` (not a thing.. yet) as well as
43            // `process_string`.
44
45            let bi = &variant.bindings()[0];
46            let ident = &bi.binding;
47            let variant_attrs = parse_variant_attributes(variant.ast().attrs)?;
48            let mut field_attrs = parse_field_attributes(0, bi.ast(), &mut true)?;
49            // `fallback_variant` is defined on the variant, so is `retain`, but downstream code
50            // expects `retain` to be set as a field attribute.
51            field_attrs.retain |= variant_attrs.retain;
52            let field_attrs_tokens = field_attrs.as_tokens(&type_attrs, Some(quote!(parent_attrs)));
53
54            let process_value = if variant_attrs.fallback_variant {
55                quote! {
56                    __processor.process_other(#ident, &__state)?;
57                }
58            } else {
59                quote! {
60                    ::relay_event_schema::processor::ProcessValue::process_value(
61                        #ident,
62                        __meta,
63                        __processor,
64                        &__state
65                    )?;
66
67                }
68            };
69            Ok(quote! {
70                let parent_attrs = __state.attrs();
71                let attrs = #field_attrs_tokens;
72                let __state = &__state.enter_nothing(
73                    Some(::std::borrow::Cow::Owned(attrs))
74                );
75
76                // This is a copy of `funcs::process_value`, due to ownership issues. In particular
77                // we want to pass the same meta twice.
78                //
79                // NOTE: Handling for ProcessingAction is slightly different (early-return). This
80                // should be fine though.
81                let action = __processor.before_process(
82                    Some(&*#ident),
83                    __meta,
84                    &__state
85                )?;
86
87                #process_value
88
89                __processor.after_process(
90                    Some(#ident),
91                    __meta,
92                    &__state
93                )?;
94            })
95        } else {
96            Ok(quote!())
97        }
98    })?;
99
100    let process_child_values_arms = s.try_each_variant(|variant| {
101        let mut is_tuple_struct = false;
102
103        if is_newtype(variant) {
104            // `process_child_values` has to be a noop because otherwise we recurse into the
105            // subtree twice due to the weird `process_value` impl
106
107            return Ok(quote!());
108        }
109
110        let mut body = TokenStream::new();
111        for (index, bi) in variant.bindings().iter().enumerate() {
112            let field_attrs = parse_field_attributes(index, bi.ast(), &mut is_tuple_struct)?;
113            let ident = &bi.binding;
114            let field_attrs_name = Ident::new(&format!("FIELD_ATTRS_{index}"), Span::call_site());
115            let field_name = field_attrs.field_name.clone();
116
117            let field_attrs_tokens = field_attrs.as_tokens(&type_attrs, None);
118
119            (quote! {
120                static #field_attrs_name: ::relay_event_schema::processor::FieldAttrs = #field_attrs_tokens;
121            })
122            .to_tokens(&mut body);
123
124            let enter_state = if field_attrs.additional_properties {
125                if is_tuple_struct {
126                    panic!("additional_properties not allowed in tuple struct");
127                }
128
129                quote! {
130                    __state.enter_nothing(Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)))
131                }
132            } else if field_attrs.flatten {
133                quote! {
134                    __state.enter_nothing(Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)))
135                }
136            } else if is_tuple_struct {
137                quote! {
138                    __state.enter_index(
139                        #index,
140                        Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)),
141                        ::relay_event_schema::processor::ValueType::for_field(#ident),
142                    )
143                }
144            } else {
145                quote! {
146                    __state.enter_borrowed(
147                        #field_name,
148                        Some(::std::borrow::Cow::Borrowed(&#field_attrs_name)),
149                        ::relay_event_schema::processor::ValueType::for_field(#ident),
150                    )
151                }
152            };
153
154            if field_attrs.additional_properties {
155                (quote! {
156                    __processor.process_other(#ident, &#enter_state)?;
157                })
158                .to_tokens(&mut body);
159            } else if field_attrs.flatten {
160                (quote! {
161                    ::relay_event_schema::processor::ProcessValue::process_child_values(
162                        #ident,
163                        __processor,
164                       &#enter_state
165                    )?;
166                })
167                .to_tokens(&mut body);
168            } else {
169                (quote! {
170                    ::relay_event_schema::processor::process_value(#ident, __processor, &#enter_state)?;
171                })
172                .to_tokens(&mut body);
173            }
174        }
175
176        Ok(quote!({ #body }))
177    })?;
178
179    let _ = s.bind_with(|_bi| synstructure::BindStyle::Ref);
180
181    let value_type_arms = s.each_variant(|variant| {
182        if !type_attrs.value_type.is_empty() {
183            let value_names = type_attrs
184                .value_type
185                .iter()
186                .map(|value_name| Ident::new(value_name, Span::call_site()));
187            quote! {
188                // enumset produces a deprecation warning because it thinks we use its internals
189                // directly, but we do actually use the macro
190                #[allow(deprecated)]
191                {
192                    enumset::enum_set!( #(::relay_event_schema::processor::ValueType::#value_names)|* )
193                }
194            }
195        } else if is_newtype(variant) {
196            let bi = &variant.bindings()[0];
197            let ident = &bi.binding;
198            quote!(::relay_event_schema::processor::ProcessValue::value_type(#ident))
199        } else {
200            quote!(::relay_event_schema::processor::EnumSet::empty())
201        }
202    });
203
204    Ok(s.gen_impl(quote! {
205        #[automatically_derived]
206        gen impl ::relay_event_schema::processor::ProcessValue for @Self {
207            fn value_type(&self) -> ::relay_event_schema::processor::EnumSet<::relay_event_schema::processor::ValueType> {
208                match *self {
209                    #value_type_arms
210                }
211            }
212
213            fn process_value<P>(
214                &mut self,
215                __meta: &mut relay_protocol::Meta,
216                __processor: &mut P,
217                __state: &::relay_event_schema::processor::ProcessingState<'_>,
218            ) -> ::relay_event_schema::processor::ProcessingResult
219            where
220                P: ::relay_event_schema::processor::Processor,
221            {
222                #process_func_call_tokens;
223                match *self {
224                    #process_value_arms
225                }
226
227                Ok(())
228            }
229
230            #[inline]
231            fn process_child_values<P>(
232                &mut self,
233                __processor: &mut P,
234                __state: &::relay_event_schema::processor::ProcessingState<'_>
235            ) -> ::relay_event_schema::processor::ProcessingResult
236            where
237                P: ::relay_event_schema::processor::Processor,
238            {
239                match *self {
240                    #process_child_values_arms
241                }
242
243                Ok(())
244            }
245        }
246    }))
247}
248
249#[derive(Default)]
250struct TypeAttrs {
251    process_func: Option<String>,
252    value_type: Vec<String>,
253    /// The default trim value for the container.
254    ///
255    /// If `trim` is specified on the container all fields of the container,
256    /// will default to this value for `trim`.
257    trim: Option<bool>,
258    /// The default pii value for the container.
259    ///
260    /// If `pii` is specified on the container, all fields of the container
261    /// will default to this value for `pii`.
262    pii: Option<Pii>,
263}
264
265impl TypeAttrs {
266    fn process_func_call_tokens(&self) -> TokenStream {
267        if let Some(ref func_name) = self.process_func {
268            let func_name = Ident::new(func_name, Span::call_site());
269            quote! {
270                __processor.#func_name(self, __meta, __state)?;
271            }
272        } else {
273            quote! {
274                self.process_child_values(__processor, __state)?;
275            }
276        }
277    }
278}
279
280fn parse_type_attributes(s: &synstructure::Structure<'_>) -> syn::Result<TypeAttrs> {
281    let mut rv = TypeAttrs::default();
282
283    for attr in &s.ast().attrs {
284        if !attr.path().is_ident("metastructure") {
285            continue;
286        }
287
288        attr.parse_nested_meta(|meta| {
289            let ident = meta.path.require_ident()?;
290
291            if ident == "process_func" {
292                let s = meta.value()?.parse::<LitStr>()?;
293                rv.process_func = Some(s.value());
294            } else if ident == "value_type" {
295                let s = meta.value()?.parse::<LitStr>()?;
296                rv.value_type.push(s.value());
297            } else if ident == "trim" {
298                let s = meta.value()?.parse::<LitBool>()?;
299                rv.trim = Some(s.value());
300            } else if ident == "pii" {
301                rv.pii = Some(meta.value()?.parse()?);
302            } else {
303                // Ignore other attributes used by `relay-protocol-derive`.
304                if !meta.input.peek(syn::Token![,]) {
305                    let _ = meta.value()?.parse::<Lit>()?;
306                }
307            }
308
309            Ok(())
310        })?;
311    }
312
313    Ok(rv)
314}
315
316#[derive(Clone, Debug)]
317enum Pii {
318    True,
319    False,
320    Maybe,
321    Dynamic(ExprPath),
322}
323
324impl syn::parse::Parse for Pii {
325    fn parse(input: ParseStream) -> syn::Result<Self> {
326        let head = input.fork();
327        let value = input.parse::<LitStr>()?;
328        let pii = match value.value().as_str() {
329            "true" => Self::True,
330            "false" => Self::False,
331            "maybe" => Self::Maybe,
332            _ => Self::Dynamic(value.parse().map_err(|_| {
333                head.error("Expected one of `true`, `false`, `maybe`, or a function name")
334            })?),
335        };
336        Ok(pii)
337    }
338}
339
340impl Pii {
341    fn as_tokens(&self) -> TokenStream {
342        match self {
343            Self::True => quote!(::relay_event_schema::processor::PiiMode::Static(
344                ::relay_event_schema::processor::Pii::True
345            )),
346            Self::False => quote!(::relay_event_schema::processor::PiiMode::Static(
347                ::relay_event_schema::processor::Pii::False
348            )),
349            Self::Maybe => quote!(::relay_event_schema::processor::PiiMode::Static(
350                ::relay_event_schema::processor::Pii::Maybe
351            )),
352            Self::Dynamic(fun) => quote!(::relay_event_schema::processor::PiiMode::Dynamic(#fun)),
353        }
354    }
355}
356
357#[derive(Clone, Copy, Debug)]
358enum Required {
359    True,
360    False,
361    ValueOrMeta,
362}
363
364impl Required {
365    fn as_tokens(&self) -> TokenStream {
366        match self {
367            Self::True => quote!(::relay_event_schema::processor::Required::Value),
368            Self::False => quote!(::relay_event_schema::processor::Required::False),
369            Self::ValueOrMeta => quote!(::relay_event_schema::processor::Required::ValueOrMeta),
370        }
371    }
372}
373
374impl syn::parse::Parse for Required {
375    fn parse(input: ParseStream) -> syn::Result<Self> {
376        let head = input.fork();
377        if let Ok(bool) = input.parse::<LitBool>() {
378            return match bool.value() {
379                true => Ok(Self::True),
380                false => Ok(Self::False),
381            };
382        }
383
384        let input = head.fork();
385        let value = input.parse::<LitStr>()?;
386
387        match value.value().as_str() {
388            "value_or_meta" => Ok(Self::ValueOrMeta),
389            _ => Err(head.error(r#"Expected one of `true`, `false`, or `"value_or_meta"`"#)),
390        }
391    }
392}
393
394#[derive(Clone, Debug)]
395enum Size {
396    Static(usize),
397    Dynamic(ExprPath),
398    Const(ExprPath),
399}
400
401impl syn::parse::Parse for Size {
402    fn parse(input: ParseStream) -> syn::Result<Self> {
403        if input.peek(LitInt) {
404            let lit = input.parse::<LitInt>()?;
405            return Ok(Self::Static(lit.base10_parse()?));
406        }
407
408        if input.peek(LitStr) {
409            let head = input.fork();
410            let path = input
411                .parse::<LitStr>()?
412                .parse()
413                .map_err(|_| head.error("Expected a function name"))?;
414            return Ok(Self::Dynamic(path));
415        }
416
417        Ok(Self::Const(input.parse::<ExprPath>()?))
418    }
419}
420
421impl Size {
422    fn as_tokens(&self) -> TokenStream {
423        match self {
424            Self::Static(n) => quote!(::relay_event_schema::processor::SizeMode::Static(Some(#n))),
425            Self::Dynamic(fun) => quote!(::relay_event_schema::processor::SizeMode::Dynamic(#fun)),
426            Self::Const(path) => {
427                quote!(::relay_event_schema::processor::SizeMode::Static(Some(#path)))
428            }
429        }
430    }
431}
432
433#[derive(Default, Debug)]
434struct VariantAttrs {
435    fallback_variant: bool,
436    retain: bool,
437}
438
439fn parse_variant_attributes(attrs: &[syn::Attribute]) -> syn::Result<VariantAttrs> {
440    let mut rv = VariantAttrs::default();
441    for attr in attrs {
442        if !attr.path().is_ident("metastructure") {
443            continue;
444        }
445
446        attr.parse_nested_meta(|meta| {
447            let ident = meta.path.require_ident()?;
448
449            if ident == "fallback_variant" {
450                rv.fallback_variant = true;
451            } else if ident == "retain" {
452                let s = meta.value()?.parse::<LitBool>()?;
453                rv.retain = s.value();
454            } else {
455                // Ignore other attributes used by `FromValue`/`IntoValue` derive macros.
456                if let Ok(v) = meta.value() {
457                    let _ = v.parse::<Lit>()?;
458                }
459            }
460
461            Ok(())
462        })?;
463    }
464
465    Ok(rv)
466}
467
468#[derive(Default, Debug)]
469struct FieldAttrs {
470    additional_properties: bool,
471    omit_from_schema: bool,
472    field_name: String,
473    flatten: bool,
474    required: Option<Required>,
475    nonempty: Option<bool>,
476    trim_whitespace: Option<bool>,
477    pii: Option<Pii>,
478    retain: bool,
479    characters: Option<TokenStream>,
480    max_chars: Option<Size>,
481    max_chars_allowance: Option<TokenStream>,
482    max_depth: Option<TokenStream>,
483    max_bytes: Option<Size>,
484    bytes_size: Option<Size>,
485    trim: Option<bool>,
486}
487
488impl FieldAttrs {
489    fn as_tokens(
490        &self,
491        type_attrs: &TypeAttrs,
492        inherit_from_field_attrs: Option<TokenStream>,
493    ) -> TokenStream {
494        let field_name = &self.field_name;
495
496        if self.required.is_none() && self.nonempty.is_some() {
497            panic!(
498                "`required` has to be explicitly set to \"true\" or \"false\" if `nonempty` is used."
499            );
500        }
501        let required = if let Some(required) = self.required {
502            required.as_tokens()
503        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
504            quote!(#parent_attrs.required)
505        } else {
506            quote!(::relay_event_schema::processor::Required::False)
507        };
508
509        let nonempty = if let Some(nonempty) = self.nonempty {
510            quote!(#nonempty)
511        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
512            quote!(#parent_attrs.nonempty)
513        } else {
514            quote!(false)
515        };
516
517        let trim_whitespace = if let Some(trim_whitespace) = self.trim_whitespace {
518            quote!(#trim_whitespace)
519        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
520            quote!(#parent_attrs.trim_whitespace)
521        } else {
522            quote!(false)
523        };
524
525        let pii = if let Some(pii) = self.pii.as_ref().or(type_attrs.pii.as_ref()) {
526            pii.as_tokens()
527        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
528            quote!(#parent_attrs.pii)
529        } else {
530            quote!(::relay_event_schema::processor::PiiMode::Static(
531                ::relay_event_schema::processor::Pii::False
532            ))
533        };
534
535        let trim = if let Some(trim) = self.trim.or(type_attrs.trim) {
536            quote!(#trim)
537        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
538            quote!(#parent_attrs.trim)
539        } else {
540            quote!(true)
541        };
542
543        let retain = self.retain;
544
545        let max_chars = if let Some(ref max_chars) = self.max_chars {
546            max_chars.as_tokens()
547        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
548            quote!(#parent_attrs.max_chars)
549        } else {
550            quote!(::relay_event_schema::processor::SizeMode::Static(None))
551        };
552
553        let max_chars_allowance = if let Some(ref max_chars_allowance) = self.max_chars_allowance {
554            quote!(#max_chars_allowance)
555        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
556            quote!(#parent_attrs.max_chars_allowance)
557        } else {
558            quote!(0)
559        };
560
561        let max_depth = if let Some(ref max_depth) = self.max_depth {
562            quote!(Some(#max_depth))
563        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
564            quote!(#parent_attrs.max_depth)
565        } else {
566            quote!(None)
567        };
568
569        let max_bytes = if let Some(ref max_bytes) = self.max_bytes {
570            max_bytes.as_tokens()
571        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
572            quote!(#parent_attrs.max_bytes)
573        } else {
574            quote!(::relay_event_schema::processor::SizeMode::Static(None))
575        };
576
577        let bytes_size = if let Some(ref bytes_size) = self.bytes_size {
578            bytes_size.as_tokens()
579        } else {
580            quote!(::relay_event_schema::processor::SizeMode::Static(None))
581        };
582
583        let characters = if let Some(ref characters) = self.characters {
584            quote!(Some(#characters))
585        } else if let Some(ref parent_attrs) = inherit_from_field_attrs {
586            quote!(#parent_attrs.characters)
587        } else {
588            quote!(None)
589        };
590
591        quote!({
592            ::relay_event_schema::processor::FieldAttrs {
593                name: Some(#field_name),
594                required: #required,
595                nonempty: #nonempty,
596                trim_whitespace: #trim_whitespace,
597                max_chars: #max_chars,
598                max_chars_allowance: #max_chars_allowance,
599                characters: #characters,
600                max_depth: #max_depth,
601                max_bytes: #max_bytes,
602                bytes_size: #bytes_size,
603                pii: #pii,
604                retain: #retain,
605                trim: #trim,
606            }
607        })
608    }
609}
610
611fn parse_field_attributes(
612    index: usize,
613    bi_ast: &syn::Field,
614    is_tuple_struct: &mut bool,
615) -> syn::Result<FieldAttrs> {
616    if bi_ast.ident.is_none() {
617        *is_tuple_struct = true;
618    } else if *is_tuple_struct {
619        panic!("invalid tuple struct");
620    }
621
622    let mut rv = FieldAttrs {
623        field_name: bi_ast
624            .ident
625            .as_ref()
626            .map(ToString::to_string)
627            .unwrap_or_else(|| index.to_string()),
628        ..Default::default()
629    };
630
631    for attr in &bi_ast.attrs {
632        if !attr.path().is_ident("metastructure") {
633            continue;
634        }
635
636        attr.parse_nested_meta(|meta| {
637            let ident = meta.path.require_ident()?;
638
639            if ident == "additional_properties" {
640                rv.additional_properties = true;
641            } else if ident == "omit_from_schema" {
642                rv.omit_from_schema = true;
643            } else if ident == "field" {
644                let s = meta.value()?.parse::<LitStr>()?;
645                rv.field_name = s.value();
646            } else if ident == "flatten" {
647                rv.flatten = true;
648            } else if ident == "required" {
649                rv.required = Some(meta.value()?.parse::<Required>()?);
650            } else if ident == "nonempty" {
651                let s = meta.value()?.parse::<LitBool>()?;
652                rv.nonempty = Some(s.value());
653            } else if ident == "trim_whitespace" {
654                let s = meta.value()?.parse::<LitBool>()?;
655                rv.trim_whitespace = Some(s.value());
656            } else if ident == "allow_chars" || ident == "deny_chars" {
657                if rv.characters.is_some() {
658                    return Err(meta.error("allow_chars and deny_chars are mutually exclusive"));
659                }
660                let s = meta.value()?.parse::<LitStr>()?;
661                rv.characters = Some(parse_character_set(ident, &s.value()));
662            } else if ident == "max_chars" {
663                rv.max_chars = Some(meta.value()?.parse()?);
664            } else if ident == "max_chars_allowance" {
665                let s = meta.value()?.parse::<LitInt>()?;
666                rv.max_chars_allowance = Some(quote!(#s));
667            } else if ident == "max_depth" {
668                let s = meta.value()?.parse::<LitInt>()?;
669                rv.max_depth = Some(quote!(#s));
670            } else if ident == "max_bytes" {
671                rv.max_bytes = Some(meta.value()?.parse()?);
672            } else if ident == "bytes_size" {
673                rv.bytes_size = Some(meta.value()?.parse()?);
674            } else if ident == "pii" {
675                rv.pii = Some(meta.value()?.parse()?);
676            } else if ident == "retain" {
677                let s = meta.value()?.parse::<LitBool>()?;
678                rv.retain = s.value();
679            } else if ident == "trim" {
680                let s = meta.value()?.parse::<LitBool>()?;
681                rv.trim = Some(s.value());
682            } else if ident == "legacy_alias" || ident == "skip_serialization" {
683                let _ = meta.value()?.parse::<Lit>()?;
684            } else {
685                return Err(meta.error("Unknown argument"));
686            }
687
688            Ok(())
689        })?;
690    }
691
692    Ok(rv)
693}
694
695fn is_newtype(variant: &synstructure::VariantInfo) -> bool {
696    variant.bindings().len() == 1 && variant.bindings()[0].ast().ident.is_none()
697}
698
699fn parse_character_set(ident: &Ident, value: &str) -> TokenStream {
700    #[derive(Clone, Copy)]
701    enum State {
702        Blank,
703        OpenRange(char),
704        MidRange(char),
705    }
706
707    let mut state = State::Blank;
708    let mut ranges = Vec::new();
709
710    for c in value.chars() {
711        match (state, c) {
712            (State::Blank, a) => state = State::OpenRange(a),
713            (State::OpenRange(a), '-') => state = State::MidRange(a),
714            (State::OpenRange(a), c) => {
715                state = State::OpenRange(c);
716                ranges.push(quote!(#a..=#a));
717            }
718            (State::MidRange(a), b) => {
719                ranges.push(quote!(#a..=#b));
720                state = State::Blank;
721            }
722        }
723    }
724
725    match state {
726        State::OpenRange(a) => ranges.push(quote!(#a..=#a)),
727        State::MidRange(a) => {
728            ranges.push(quote!(#a..=#a));
729            ranges.push(quote!('-'..='-'));
730        }
731        State::Blank => {}
732    }
733
734    let is_negative = ident == "deny_chars";
735
736    quote! {
737        ::relay_event_schema::processor::CharacterSet {
738            char_is_valid: |c: char| -> bool {
739                match c {
740                    #((#ranges) => !#is_negative,)*
741                    _ => #is_negative,
742                }
743            },
744            ranges: &[ #(#ranges,)* ],
745            is_negative: #is_negative,
746        }
747    }
748}