Skip to main content

hblank_macros/
lib.rs

1//! Procedural macros for Hblank.
2
3use proc_macro::TokenStream;
4use quote::{format_ident, quote};
5use syn::{
6    Attribute, Data, DeriveInput, Error, Expr, ExprLit, Fields, FnArg, ItemFn, Lit, LitStr, Meta,
7    Path, Type,
8    parse::{Parse, ParseStream},
9    parse_macro_input,
10};
11
12#[proc_macro_derive(HblankProps, attributes(hblank))]
13pub fn derive_hblank_props(input: TokenStream) -> TokenStream {
14    let input = parse_macro_input!(input as DeriveInput);
15    expand_hblank_props(input)
16        .unwrap_or_else(Error::into_compile_error)
17        .into()
18}
19
20fn expand_hblank_props(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
21    let name = input.ident;
22    let Data::Struct(data) = input.data else {
23        return Err(Error::new_spanned(
24            name,
25            "HblankProps can only be derived for structs with named fields",
26        ));
27    };
28    let Fields::Named(fields) = data.fields else {
29        return Err(Error::new_spanned(
30            name,
31            "HblankProps requires named fields",
32        ));
33    };
34
35    let mut definitions = Vec::with_capacity(fields.named.len());
36    let mut readers = Vec::with_capacity(fields.named.len());
37    let mut writers = Vec::with_capacity(fields.named.len());
38
39    for field in fields.named {
40        let options = field_options(&field.attrs)?;
41        if options.skip {
42            continue;
43        }
44        let ident = field
45            .ident
46            .ok_or_else(|| Error::new_spanned(&field.ty, "HblankProps requires named fields"))?;
47        let ty = field.ty;
48        let id = ident.to_string();
49        let kind = control_kind(&ty, &options);
50        let label = options.label.unwrap_or_else(|| humanize(&id));
51        let docs = docs(&field.attrs);
52        let definition = quote! {
53            ::hblank::ControlDefinition {
54                id: #id,
55                label: #label,
56                docs: #docs,
57                kind: #kind,
58            }
59        };
60        let (reader, writer) =
61            control_accessors(&ty, &ident, &id, &definition, options.adapter.as_ref());
62
63        definitions.push(definition);
64        readers.push(reader);
65        writers.push(writer);
66    }
67
68    Ok(quote! {
69        impl ::hblank::HblankProps for #name {
70            fn definitions(&self) -> &'static [::hblank::ControlDefinition] {
71                const DEFINITIONS: &[::hblank::ControlDefinition] = &[
72                    #(#definitions),*
73                ];
74                DEFINITIONS
75            }
76
77            fn control_value(&self, id: &str) -> Option<::hblank::ControlValue> {
78                match id {
79                    #(#readers,)*
80                    _ => None,
81                }
82            }
83
84            fn set_control(
85                &mut self,
86                id: &str,
87                value: ::hblank::ControlValue,
88            ) -> Result<(), ::hblank::ControlError> {
89                match id {
90                    #(#writers,)*
91                    _ => Err(::hblank::ControlError::UnknownControl(id.to_owned())),
92                }
93            }
94
95            fn clone_box(&self) -> Box<dyn ::hblank::HblankProps> {
96                Box::new(self.clone())
97            }
98
99            fn as_any(&self) -> &dyn ::std::any::Any {
100                self
101            }
102        }
103    })
104}
105
106#[proc_macro_derive(HblankEnum, attributes(hblank))]
107pub fn derive_hblank_enum(input: TokenStream) -> TokenStream {
108    let input = parse_macro_input!(input as DeriveInput);
109    expand_hblank_enum(input)
110        .unwrap_or_else(Error::into_compile_error)
111        .into()
112}
113
114fn expand_hblank_enum(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
115    let name = input.ident;
116    let Data::Enum(data) = input.data else {
117        return Err(Error::new_spanned(
118            name,
119            "HblankEnum can only be derived for enums",
120        ));
121    };
122
123    let mut variants = Vec::with_capacity(data.variants.len());
124    let mut names = Vec::with_capacity(data.variants.len());
125    for variant in data.variants {
126        if !matches!(variant.fields, Fields::Unit) {
127            return Err(Error::new_spanned(
128                variant,
129                "HblankEnum variants cannot contain data",
130            ));
131        }
132        let ident = variant.ident;
133        let label = field_label(&variant.attrs)?.unwrap_or_else(|| humanize(&ident.to_string()));
134        variants.push(ident);
135        names.push(label);
136    }
137
138    Ok(quote! {
139        impl ::hblank::HblankEnum for #name {
140            const VARIANTS: &'static [&'static str] = &[#(#names),*];
141
142            fn variant_name(&self) -> &'static str {
143                match self {
144                    #(Self::#variants => #names),*
145                }
146            }
147
148            fn from_variant_name(value: &str) -> Option<Self> {
149                match value {
150                    #(#names => Some(Self::#variants),)*
151                    _ => None,
152                }
153            }
154        }
155    })
156}
157
158#[derive(Default)]
159struct ComponentArgs {
160    title: Option<LitStr>,
161    group: Option<LitStr>,
162    docs: Option<Path>,
163    handle: Option<Type>,
164}
165
166#[proc_macro_attribute]
167pub fn component(args: TokenStream, input: TokenStream) -> TokenStream {
168    let mut component_args = ComponentArgs::default();
169    let parser = syn::meta::parser(|meta| {
170        if meta.path.is_ident("title") {
171            component_args.title = Some(meta.value()?.parse()?);
172        } else if meta.path.is_ident("group") {
173            component_args.group = Some(meta.value()?.parse()?);
174        } else if meta.path.is_ident("docs") {
175            component_args.docs = Some(meta.value()?.parse()?);
176        } else if meta.path.is_ident("handle") {
177            component_args.handle = Some(meta.value()?.parse()?);
178        } else {
179            return Err(meta.error("expected one of: title, group, docs, handle"));
180        }
181        Ok(())
182    });
183    syn::parse_macro_input!(args with parser);
184    let function = parse_macro_input!(input as ItemFn);
185    expand_component(component_args, &function)
186        .unwrap_or_else(Error::into_compile_error)
187        .into()
188}
189
190fn expand_component(
191    args: ComponentArgs,
192    function: &ItemFn,
193) -> syn::Result<proc_macro2::TokenStream> {
194    validate_synchronous_non_generic(function, "components")?;
195    let props_type = render_props_type(function, "components")?;
196    let function_name = &function.sig.ident;
197    let module_name = format_ident!("__hblank_component_{}", function_name);
198    let function_docs = docs(&function.attrs);
199    let title = args.title.unwrap_or_else(|| {
200        LitStr::new(&humanize(&function_name.to_string()), function_name.span())
201    });
202    let group = args
203        .group
204        .map_or_else(|| quote!(module_path!()), |group| quote!(#group));
205    let doc_page = args
206        .docs
207        .map_or_else(|| quote!(), |docs| quote!(.with_docs(#docs())));
208    let handle_helper = args.handle.map_or_else(
209        || quote!(),
210        |handle| {
211            quote! {
212                pub(crate) fn render_with_handle(
213                    props: &#props_type,
214                    window: &mut ::hblank::gpui::Window,
215                    cx: &mut ::hblank::gpui::App,
216                ) -> (::hblank::gpui::AnyElement, #handle) {
217                    super::#function_name(props, window, cx).into_erased_parts()
218                }
219            }
220        },
221    );
222
223    Ok(quote! {
224        #function
225
226        #[doc(hidden)]
227        pub(crate) mod #module_name {
228            use super::*;
229
230            pub(crate) fn id() -> ::std::string::String {
231                ::hblank::canonical_source_id(file!(), stringify!(#function_name))
232            }
233
234            pub(crate) fn assert_props(_: &#props_type) {}
235
236            #handle_helper
237
238            pub(crate) fn build() -> ::hblank::ComponentDefinition {
239                fn render(
240                    props: &dyn ::hblank::HblankProps,
241                    window: &mut ::hblank::gpui::Window,
242                    cx: &mut ::hblank::gpui::App,
243                ) -> ::hblank::gpui::AnyElement {
244                    let props = props
245                        .as_any()
246                        .downcast_ref::<#props_type>()
247                        .expect("Hblank component received the wrong props type");
248                    ::hblank::gpui::IntoElement::into_any_element(
249                        super::#function_name(props, window, cx),
250                    )
251                }
252
253                ::hblank::ComponentDefinition::new::<#props_type>(
254                    ::hblank::ComponentMetadata {
255                        id: id(),
256                        title: #title,
257                        group: #group,
258                        docs: #function_docs,
259                        declaration: stringify!(#function),
260                        source: file!(),
261                        line: line!(),
262                    },
263                    render,
264                )
265                #doc_page
266            }
267        }
268
269        ::hblank::__private::inventory::submit! {
270            ::hblank::ComponentRegistration { build: #module_name::build }
271        }
272    })
273}
274
275struct RenderHandleInput {
276    component: Path,
277    props: Expr,
278    window: Expr,
279    cx: Expr,
280}
281
282impl Parse for RenderHandleInput {
283    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
284        let component = input.parse()?;
285        input.parse::<syn::Token![,]>()?;
286        let props = input.parse()?;
287        input.parse::<syn::Token![,]>()?;
288        let window = input.parse()?;
289        input.parse::<syn::Token![,]>()?;
290        let cx = input.parse()?;
291        Ok(Self {
292            component,
293            props,
294            window,
295            cx,
296        })
297    }
298}
299
300#[proc_macro]
301pub fn render_handle(input: TokenStream) -> TokenStream {
302    let input = parse_macro_input!(input as RenderHandleInput);
303    let props = input.props;
304    let window = input.window;
305    let cx = input.cx;
306    match component_module_path(&input.component) {
307        Ok(module) => quote!(#module::render_with_handle(#props, #window, #cx)).into(),
308        Err(error) => error.into_compile_error().into(),
309    }
310}
311
312struct CustomDocInput {
313    renderer: Path,
314    payload: Expr,
315}
316
317impl Parse for CustomDocInput {
318    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
319        let renderer = input.parse()?;
320        input.parse::<syn::Token![,]>()?;
321        let payload = input.parse()?;
322        Ok(Self { renderer, payload })
323    }
324}
325
326#[proc_macro_attribute]
327pub fn doc_block(args: TokenStream, input: TokenStream) -> TokenStream {
328    if !args.is_empty() {
329        return Error::new(
330            proc_macro2::Span::call_site(),
331            "Hblank custom doc blocks take no attributes",
332        )
333        .into_compile_error()
334        .into();
335    }
336    let function = parse_macro_input!(input as ItemFn);
337    let function_name = &function.sig.ident;
338    let module_name = format_ident!("__hblank_doc_block_{}", function_name);
339    quote! {
340        #function
341
342        #[doc(hidden)]
343        pub(crate) mod #module_name {
344            pub(crate) fn id() -> ::std::string::String {
345                concat!(module_path!(), "::", stringify!(#function_name)).to_owned()
346            }
347
348            pub(crate) const RENDER: ::hblank::CustomDocRenderer = super::#function_name;
349        }
350
351        ::hblank::__private::inventory::submit! {
352            ::hblank::CustomDocBlockRegistration {
353                id: concat!(module_path!(), "::", stringify!(#function_name)),
354                render: #module_name::RENDER,
355            }
356        }
357    }
358    .into()
359}
360
361#[proc_macro]
362pub fn custom_doc(input: TokenStream) -> TokenStream {
363    let input = parse_macro_input!(input as CustomDocInput);
364    let payload = input.payload;
365    match doc_block_module_path(&input.renderer) {
366        Ok(module) => quote!(::hblank::DocBlock::custom(#module::id(), #payload)).into(),
367        Err(error) => error.into_compile_error().into(),
368    }
369}
370
371#[proc_macro_attribute]
372pub fn theme_hook(args: TokenStream, input: TokenStream) -> TokenStream {
373    if !args.is_empty() {
374        return Error::new(
375            proc_macro2::Span::call_site(),
376            "Hblank theme hooks take no attributes",
377        )
378        .into_compile_error()
379        .into();
380    }
381    let function = parse_macro_input!(input as ItemFn);
382    let function_name = &function.sig.ident;
383    quote! {
384        #function
385
386        const _: ::hblank::ThemeHook = #function_name;
387
388        ::hblank::__private::inventory::submit! {
389            ::hblank::ThemeHookRegistration {
390                id: concat!(module_path!(), "::", stringify!(#function_name)),
391                apply: #function_name,
392            }
393        }
394    }
395    .into()
396}
397
398#[derive(Default)]
399struct FixtureArgs {
400    component: Option<Path>,
401    title: Option<LitStr>,
402}
403
404#[proc_macro]
405pub fn fixture_ref(input: TokenStream) -> TokenStream {
406    let fixture = parse_macro_input!(input as Path);
407    match fixture_module_path(&fixture) {
408        Ok(module) => quote!(#module::id()).into(),
409        Err(error) => error.into_compile_error().into(),
410    }
411}
412
413#[proc_macro_attribute]
414pub fn fixture(args: TokenStream, input: TokenStream) -> TokenStream {
415    let mut fixture_args = FixtureArgs::default();
416    let parser = syn::meta::parser(|meta| {
417        if meta.path.is_ident("component") {
418            fixture_args.component = Some(meta.value()?.parse()?);
419        } else if meta.path.is_ident("title") {
420            fixture_args.title = Some(meta.value()?.parse()?);
421        } else {
422            return Err(meta.error("expected one of: component, title"));
423        }
424        Ok(())
425    });
426    syn::parse_macro_input!(args with parser);
427    let function = parse_macro_input!(input as ItemFn);
428    expand_fixture(fixture_args, &function)
429        .unwrap_or_else(Error::into_compile_error)
430        .into()
431}
432
433fn expand_fixture(args: FixtureArgs, function: &ItemFn) -> syn::Result<proc_macro2::TokenStream> {
434    validate_synchronous_non_generic(function, "fixtures")?;
435    if !function.sig.inputs.is_empty() {
436        return Err(Error::new_spanned(
437            &function.sig.inputs,
438            "Hblank fixture variants take no arguments and return component props",
439        ));
440    }
441    let component = args.component.ok_or_else(|| {
442        Error::new_spanned(
443            &function.sig.ident,
444            "Hblank fixture variants require component = path::to::component",
445        )
446    })?;
447    let component_module = component_module_path(&component)?;
448    let function_name = &function.sig.ident;
449    let module_name = format_ident!("__hblank_fixture_{}", function_name);
450    let function_docs = docs(&function.attrs);
451    let title = args.title.unwrap_or_else(|| {
452        LitStr::new(&humanize(&function_name.to_string()), function_name.span())
453    });
454
455    Ok(quote! {
456        #function
457
458        #[doc(hidden)]
459        pub(crate) mod #module_name {
460            use super::*;
461
462            pub(crate) fn id() -> ::std::string::String {
463                ::hblank::canonical_source_id(file!(), stringify!(#function_name))
464            }
465
466            pub(crate) fn build() -> ::hblank::FixtureRegistrationData {
467                let defaults = super::#function_name();
468                #component_module::assert_props(&defaults);
469                ::hblank::FixtureRegistrationData::new(
470                    ::hblank::FixtureRegistrationMetadata {
471                        id: id(),
472                        title: #title,
473                        docs: #function_docs,
474                        declaration: stringify!(#function),
475                        source: file!(),
476                        line: line!(),
477                    },
478                    #component_module::id(),
479                    ::std::boxed::Box::new(defaults),
480                )
481            }
482        }
483
484        ::hblank::__private::inventory::submit! {
485            ::hblank::FixtureRegistration { build: #module_name::build }
486        }
487    })
488}
489
490fn validate_synchronous_non_generic(function: &ItemFn, subject: &str) -> syn::Result<()> {
491    if function.sig.asyncness.is_some() {
492        return Err(Error::new_spanned(
493            &function.sig,
494            format!("Hblank {subject} must be synchronous"),
495        ));
496    }
497    if !function.sig.generics.params.is_empty() {
498        return Err(Error::new_spanned(
499            &function.sig.generics,
500            format!("Hblank {subject} cannot be generic"),
501        ));
502    }
503    Ok(())
504}
505
506fn render_props_type<'a>(function: &'a ItemFn, subject: &str) -> syn::Result<&'a Type> {
507    if function.sig.inputs.len() != 3 {
508        return Err(Error::new_spanned(
509            &function.sig.inputs,
510            format!("Hblank {subject} take exactly (&Props, &mut gpui::Window, &mut gpui::App)"),
511        ));
512    }
513    let first = function
514        .sig
515        .inputs
516        .first()
517        .ok_or_else(|| Error::new_spanned(&function.sig, "missing props argument"))?;
518    let FnArg::Typed(first) = first else {
519        return Err(Error::new_spanned(
520            first,
521            format!("the first Hblank {subject} argument must be &Props"),
522        ));
523    };
524    let Type::Reference(props_reference) = first.ty.as_ref() else {
525        return Err(Error::new_spanned(
526            &first.ty,
527            format!("the first Hblank {subject} argument must be &Props"),
528        ));
529    };
530    if props_reference.mutability.is_some() {
531        return Err(Error::new_spanned(
532            &first.ty,
533            "component props are immutable; mutate them through harness controls",
534        ));
535    }
536    Ok(props_reference.elem.as_ref())
537}
538
539fn doc_block_module_path(renderer: &Path) -> syn::Result<Path> {
540    let mut module = renderer.clone();
541    let Some(last) = module.segments.last_mut() else {
542        return Err(Error::new_spanned(
543            renderer,
544            "doc block renderer path cannot be empty",
545        ));
546    };
547    last.ident = format_ident!("__hblank_doc_block_{}", last.ident);
548    Ok(module)
549}
550
551fn fixture_module_path(fixture: &Path) -> syn::Result<Path> {
552    let mut module = fixture.clone();
553    let Some(last) = module.segments.last_mut() else {
554        return Err(Error::new_spanned(fixture, "fixture path cannot be empty"));
555    };
556    last.ident = format_ident!("__hblank_fixture_{}", last.ident);
557    Ok(module)
558}
559
560fn component_module_path(component: &Path) -> syn::Result<Path> {
561    let mut module = component.clone();
562    let Some(last) = module.segments.last_mut() else {
563        return Err(Error::new_spanned(
564            component,
565            "component path cannot be empty",
566        ));
567    };
568    last.ident = format_ident!("__hblank_component_{}", last.ident);
569    Ok(module)
570}
571
572fn docs(attributes: &[Attribute]) -> String {
573    attributes
574        .iter()
575        .filter_map(|attribute| {
576            if !attribute.path().is_ident("doc") {
577                return None;
578            }
579            let Meta::NameValue(name_value) = &attribute.meta else {
580                return None;
581            };
582            let Expr::Lit(ExprLit {
583                lit: Lit::Str(value),
584                ..
585            }) = &name_value.value
586            else {
587                return None;
588            };
589            Some(value.value().trim().to_owned())
590        })
591        .collect::<Vec<_>>()
592        .join("\n")
593}
594
595#[derive(Default)]
596struct FieldOptions {
597    label: Option<String>,
598    skip: bool,
599    multiline: bool,
600    min: Option<f64>,
601    max: Option<f64>,
602    step: Option<f64>,
603    adapter: Option<Path>,
604}
605
606impl FieldOptions {
607    const fn has_number_constraints(&self) -> bool {
608        self.min.is_some() || self.max.is_some() || self.step.is_some()
609    }
610}
611
612fn control_kind(ty: &Type, options: &FieldOptions) -> proc_macro2::TokenStream {
613    let mut kind = options.adapter.as_ref().map_or_else(
614        || quote!(<#ty as ::hblank::__private::ControlField>::KIND),
615        |adapter| {
616            quote!(
617                <<#adapter as ::hblank::HblankControlAdapter<#ty>>::Value
618                    as ::hblank::__private::ControlField>::KIND
619            )
620        },
621    );
622    if options.multiline {
623        kind = quote!((#kind).multiline());
624    }
625    if options.has_number_constraints() {
626        let min = option_f64(options.min);
627        let max = option_f64(options.max);
628        let step = options.step.unwrap_or(1.0);
629        kind = quote! {
630            (#kind).constrained(::hblank::NumberConstraints {
631                min: #min,
632                max: #max,
633                step: #step,
634            })
635        };
636    }
637    kind
638}
639
640fn control_accessors(
641    ty: &Type,
642    ident: &syn::Ident,
643    id: &str,
644    definition: &proc_macro2::TokenStream,
645    adapter: Option<&Path>,
646) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
647    adapter.map_or_else(
648        || {
649            (
650                quote! {
651                    #id => Some(
652                        <#ty as ::hblank::__private::ControlField>::to_control_value(&self.#ident)
653                    )
654                },
655                quote! {
656                    #id => {
657                        let definition = #definition;
658                        definition.validate(&value)?;
659                        <#ty as ::hblank::__private::ControlField>::set_control_value(
660                            &mut self.#ident,
661                            #id,
662                            value,
663                        )
664                    }
665                },
666            )
667        },
668        |adapter| {
669            (
670                quote! {
671                    #id => {
672                        let control = <#adapter as ::hblank::HblankControlAdapter<#ty>>::to_control(
673                            &self.#ident,
674                        );
675                        Some(
676                            <<#adapter as ::hblank::HblankControlAdapter<#ty>>::Value
677                                as ::hblank::__private::ControlField>::to_control_value(&control)
678                        )
679                    }
680                },
681                quote! {
682                    #id => {
683                        let definition = #definition;
684                        definition.validate(&value)?;
685                        let mut control =
686                            <#adapter as ::hblank::HblankControlAdapter<#ty>>::to_control(
687                                &self.#ident,
688                            );
689                        <<#adapter as ::hblank::HblankControlAdapter<#ty>>::Value
690                            as ::hblank::__private::ControlField>::set_control_value(
691                                &mut control,
692                                #id,
693                                value,
694                            )?;
695                        <#adapter as ::hblank::HblankControlAdapter<#ty>>::apply_control(
696                            &mut self.#ident,
697                            control,
698                        );
699                        Ok(())
700                    }
701                },
702            )
703        },
704    )
705}
706
707fn field_options(attributes: &[Attribute]) -> syn::Result<FieldOptions> {
708    let mut options = FieldOptions::default();
709    for attribute in attributes {
710        if !attribute.path().is_ident("hblank") {
711            continue;
712        }
713        attribute.parse_nested_meta(|meta| {
714            if meta.path.is_ident("label") {
715                let value: LitStr = meta.value()?.parse()?;
716                options.label = Some(value.value());
717            } else if meta.path.is_ident("skip") {
718                options.skip = true;
719            } else if meta.path.is_ident("multiline") {
720                options.multiline = true;
721            } else if meta.path.is_ident("min") {
722                options.min = Some(parse_number(meta.value()?.parse()?)?);
723            } else if meta.path.is_ident("max") {
724                options.max = Some(parse_number(meta.value()?.parse()?)?);
725            } else if meta.path.is_ident("step") {
726                options.step = Some(parse_number(meta.value()?.parse()?)?);
727            } else if meta.path.is_ident("adapter") {
728                options.adapter = Some(meta.value()?.parse()?);
729            } else {
730                return Err(
731                    meta.error("expected one of: label, skip, multiline, min, max, step, adapter")
732                );
733            }
734            Ok(())
735        })?;
736    }
737    if let Some(step) = options.step
738        && (!step.is_finite() || step <= 0.0)
739    {
740        return Err(Error::new(
741            proc_macro2::Span::call_site(),
742            "control step must be finite and greater than zero",
743        ));
744    }
745    if options.min.is_some_and(|value| !value.is_finite())
746        || options.max.is_some_and(|value| !value.is_finite())
747    {
748        return Err(Error::new(
749            proc_macro2::Span::call_site(),
750            "control bounds must be finite",
751        ));
752    }
753    if let (Some(min), Some(max)) = (options.min, options.max)
754        && min > max
755    {
756        return Err(Error::new(
757            proc_macro2::Span::call_site(),
758            "control min cannot exceed max",
759        ));
760    }
761    Ok(options)
762}
763
764fn parse_number(expression: Expr) -> syn::Result<f64> {
765    match expression {
766        Expr::Lit(ExprLit {
767            lit: Lit::Int(value),
768            ..
769        }) => value.base10_parse(),
770        Expr::Lit(ExprLit {
771            lit: Lit::Float(value),
772            ..
773        }) => value.base10_parse(),
774        Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
775            Ok(-parse_number(*unary.expr)?)
776        }
777        expression => Err(Error::new_spanned(expression, "expected a numeric literal")),
778    }
779}
780
781fn option_f64(value: Option<f64>) -> proc_macro2::TokenStream {
782    value.map_or_else(|| quote!(None), |value| quote!(Some(#value)))
783}
784
785fn field_label(attributes: &[Attribute]) -> syn::Result<Option<String>> {
786    Ok(field_options(attributes)?.label)
787}
788
789fn humanize(identifier: &str) -> String {
790    let mut output = String::with_capacity(identifier.len() + 4);
791    let mut previous_lowercase = false;
792    for (index, character) in identifier.chars().enumerate() {
793        if character == '_' || character == '-' {
794            if !output.ends_with(' ') {
795                output.push(' ');
796            }
797            previous_lowercase = false;
798            continue;
799        }
800        if character.is_uppercase() && previous_lowercase {
801            output.push(' ');
802        }
803        if index == 0 {
804            output.extend(character.to_uppercase());
805        } else {
806            output.push(character);
807        }
808        previous_lowercase = character.is_lowercase();
809    }
810    output
811}