#declarative #builder #dsl #template #struct-literal

nightly template-builder

A rust library for making idiomatic, declarative, builder-like patterns that use the struct literal syntax

1 unstable release

0.1.0 Jun 21, 2022

#2568 in Rust patterns

LGPL-3.0

8KB
60 lines

Template.rs

A rust library for making idiomatic, declarative, builder-like patterns that use the struct literal syntax.

No function-like macros required!

How to use

To make a template, just as you would make a builder, you have to consider the following:

  1. All the parameters
  2. The default state of the template
  3. What the template builds into.
  4. Its relationship with other objects.

You can define a template using the #[template] annotation to your template model struct, and the Template trait.

1. Defining the parameters

Simply use the the #[template] annotation on a struct with all the parameters as its fields

#[template]
pub struct Box {
    orientation: Orientation,
    spacing: i32,
    padding: i32,
    margin: i32,
}

#[template]
pub struct Button {
    padding: i32,
    margin: i32,
    style: StyleDescriptor,
    text: String
}

2. The default state of the template

By default, this library defines the default state of your template by #[derive(Default)]. Custom Default implementations are also planned.

3. What the template builds into

Use the Template trait to define what the template builds into, and how


impl Template for Box {
    type Output = some_other_lib::Box;
    
    fn define(self) -> Self::Output {
        let mut this = some_other_lib::Box::new();
        this.padding = self.padding;
        //...
        this
    }
}

impl Template for Button {
    type Output = some_other_lib::Button;
    
    fn define(self) -> Self::Output {
        let mut this = some_other_lib::Button::new();
        this.padding = self.padding;
        //...
        this
    }
}

4. Its relationship with other objects.

Define different default states by dependency injection by objects relative to the template's target.

trait Container {
    fn child<T, W>(&self) -> T
        where T: Template<Output = W>,
              W: some_other_lib::Widget;
}

impl Container for some_other_lib::Box {
    fn child<T, W>(&self) -> T
        where T: Template<Output = W>,
              W: some_other_lib::Widget 
    {
        let this = self.clone();
        let out = Button::default();            // The template
        out.on_create(move |w| this.add(w));    // provided by the #[template] annotation
        out
    }
}

Using the template

All annotated templates implement the following traits for building the target types:

  • for<A, F: FnOnce(Self::Output) -> A> FnOnce(F) -> A
  • FnOnce() -> Self::Output

Those traits can be invoked right after the struct literal in a "currying" fashion.

This is a simple example of idiomatic templates:

Box {
    orientation: Orientation::HORIZONTAL,
    padding: 6,
    spacing: 6,
    ..Default::default()
} (|w| {
    Box {
        orientation: Orientation::VERTICAL,
        spacing: 6,
        ..w.child()
    } (|w| {

        Button {
            text: "Column btn 1",
            ..w.child()
        }();    
        // Function call constructs button 
        // and adds it to the box as per the
        // ..w.child() injection directive

        Button {
            text: "Column btn 2"
            ..w.child()
        }();

    }); // function call runs the lambda argument
        // on the output and then returns it

    Button {
        text: "Big btn",
        ..w.child()
    }();
})

// expected result:
// |--------------|---------|
// | Column btn 1 |         |
// |--------------| Big Btn |
// | Column btn 2 |         |
// |--------------|---------|

In case you don't want to use direct function calls, you can use the following equivalent methods:

  • fn build<A>(self, F: impl FnOnce(Self::Output) -> A) -> A
  • fn create(self) -> Self::Output
Box {
    orientation: Orientation::HORIZONTAL,
    padding: 6,
    spacing: 6,
    ..Default::default()
}.build(|w| {       // creations with lambdas use the "build" method
    Button {
        text: "Hello World",
        ..w.child()
    }.create();     // creations without arguments use the "create" method
})

Current Problems and Future Plans

  1. Allow the user to define custom default states for templates
  2. Create a Templatable trait associates the Output of a template with its template, so that you don't have to associate it yourself. Eg.:
trait Templatable {
    type New: Template<Output = Self>;
}

// ...so that

use some_other_lib::Box;

Box::New {
    //...
} ();
  1. Currently some type analysers do not consider std::ops::FnOnce implementations, so they automatically label Struct { /*...*/ } ( /*...*/ ) syntaxes as wrong when they are perfectly legal and fine as per the std::ops::FnOnce trait definition. For example, in the jetbrains Rust Plugin. Until such problems are solved, either use a different analyser, or use the alternative .build() and .create() methods.

Dependencies

~7KB