Attribute Macro laby::laby

source ·
#[laby]
Expand description

Generates a macro that calls a function with named arguments.

Named arguments can be useful when a function accepts several arguments, because explicitly stating the arguments with parameter names can improve readability.

This attribute macro generates a function-like macro, with the same visibility and path as the target function, which allows callers to call that function with the arguments specified in any arbitrary order using assignment-like expressions (name = value).

Although this attribute is provided for use in laby components, its implementation is not specific to laby. It may be applied to any function, albeit with some caveats documented below. Refer to the crate-level documentation for more usage examples.

#[default] arguments

By default, all arguments must be specified explicitly, even Option<T> types. Omittable arguments are opt-in. To mark a parameter as omittable, prepend the #[default] attribute to the parameter.

#[laby]
fn foo(x: Option<&str>) {
    assert!(x.is_none());
}

#[laby]
fn bar(#[default] x: Option<&str>) {
    assert!(x.is_none());
}

foo!(x = None); // required
bar!(x = None); // omittable
bar!(); // omitted; equivalent to the above line

This attribute by default defaults to Default::default(). This behavior can be customized by passing a default expression as the attribute argument. The expression is evaluated in the macro expansion context.

#[laby]
fn test(left: &str, #[default("b")] right: &str) {
    assert_eq!(left, right);
}

test!(left = "a", right = "a");
test!(left = "b", right = "b");
test!(left = "b"); // omitted; equivalent to the above line

It is not possible to apply #[default] on generic parameters like impl Render because the compiler cannot infer which default implementation of Render should be used. This can be circumvented by using the unit type () implementation of Render as the default expression, which simply renders nothing.

#[laby]
fn component(#[default(())] title: impl Render) -> impl Render {
    article!(
        h1!(title),
    )
}

assert_eq!(render!(component!()), "<article><h1></h1></article>");
assert_eq!(render!(component!(title = a!("title"))), "<article><h1><a>title</a></h1></article>");

#[rest] arguments

By default, all arguments must be specified with their respective parameter name. A function may declare at most one parameter with this attribute, which binds all arguments without a specified name to that parameter, wrapped together using frag!. This behavior is similar to React children.

#[laby]
fn component(#[default(())] title: impl Render, #[rest] children: impl Render) -> impl Render {
    article!(
        h1!(title),
        main!(children),
    )
}

assert_eq!(render!(component!()), "<article><h1></h1><main></main></article>");
assert_eq!(render!(component!("para")), "<article><h1></h1><main>para</main></article>");
assert_eq!(render!(component!(p!("para1"), p!("para2"))), "<article><h1></h1><main><p>para1</p><p>para2</p></main></article>");
assert_eq!(render!(component!(title = "laby", p!("para1"), p!("para2"))), "<article><h1>laby</h1><main><p>para1</p><p>para2</p></main></article>");

Caveats

Function must be free-standing

The target function with this attribute must be free-standing; it must be declared at the module-level, not within a trait or impl block. This is because Rust simply does not support macro declarations in such places.

struct Foo;

#[laby]
fn good(x: &Foo) {}

impl Foo {
    // this will not compile:
    #[laby]
    fn bad(&self) {}
}

Function should not be named after an HTML tag

When a markup macro named after an HTML tag is invoked within another markup macro, laby recognizes this pattern and inlines that nested HTML macro into the parent macro as an optimization, regardless of whether that HTML macro is indeed an HTML macro or another macro with a conflicting name that actually does something completely different. As a workaround, you may alias the function with a different name.

#[laby]
fn article() -> impl Render {
    "foo"
}

fn good() {
    use article as foo;

    let s = render!(div!(foo!()));
    assert_eq!(s, "<div>foo</div>");
}

fn bad() {
    // refers to `laby::article`, not the `article` macro declared above!
    let s = render!(div!(article!()));
    assert_eq!(s, "<div><article></article></div>");
}

Macro must be imported into scope

When calling the target function using the generated macro, both that function and the macro must be imported directly into the current scope. It cannot be called by relative or fully qualified paths. This is due to hygiene limitations of macro_rules! which prevent functions from being referenced within macros unambiguously.

This caveat can be circumvented by enabling the decl_macro feature.

mod foo {
    #[laby]
    pub fn bar() {}
}

fn good() {
    use foo::bar;
    bar!();
}

fn bad() {
    foo::bar!(); // no function named `bar` in scope
}

Macro is not exported outside the crate

The generated macro is defined using macro_rules! which prevents macros from being exported in modules other than the crate root. Due to this limitation, the maximum visibility of the generated macro is restricted to pub(crate) even if the target function is pub.

This caveat can be circumvented by enabling the decl_macro feature.

// crate_a
#[laby]
pub fn foo() {}

fn good() {
    foo!();
}

// crate_b
fn bad() {
    use crate_a::foo; // macro `foo` is private
    foo!();
}

Macros 2.0 support

laby comes with support for the experimental Declarative Macros 2.0 compiler feature which can be enabled using the feature flag decl_macro. This requires a nightly toolchain.

To enable this feature, add laby’s feature flag in your Cargo.toml,

[dependencies]
laby = { version = "...", features = ["decl_macro"] }

and enable the compiler’s feature flag in your crate root.

#![feature(decl_macro)]

The generated macros will now use the new macro foo { ... } syntax instead of macro_rules! foo { ... }.