3 unstable releases

0.3.0 Dec 12, 2023
0.0.2 Oct 5, 2023
0.0.1 Oct 5, 2023

#1119 in Data structures

MIT/Apache

30KB
447 lines

Introduction

constructivism is a Rust sample-library designed to simplify the construction of structured data by defining and manipulating sequences of Constructs. This README provides an overview of how to use constructivism and how it can be inlined into you project using constructivist library.

Installation

To use Constructivism in your Rust project, add it as a dependency in your Cargo.toml file:

[dependencies]
constructivism = "0.0.2"

Or let the cargo do the stuff:

cargo add constructivism

Constructivism can be inlined into you library as for example your_library_constructivism within constructivist crate. See instructions.

Guide

See also examples/tutorial.rs

Getting Started

Usually you start with

use constructivism::*;

Constructs and Sequences

1.1. Constructs: Constructivism revolves around the concept of Constructs. You can derive construct like this:

#[derive(Construct)]
pub struct Node {
    hidden: bool,
    position: (f32, f32),
}

1.2 construct!: You can use the construct! macro to create instances of Constructs. Please note the dots at the beginning of the each param, they are required and you will find this syntax quite useful.

fn create_node() {
    let node = construct!(Node {
        .position: (10., 10.),
        .hidden: true
    });
    assert_eq!(node.position.0, 10.);
    assert_eq!(node.hidden, true);
}

1.3 Sequences: A Construct can be declared only in front of another Construct. constructivism comes with only Nothing, () construct. The Self -> Base relation called Sequence in constructivism. You can omit the Sequence declaration, Self -> Nothing used in this case. If you want to derive Construct on the top of another meaningful Construct, you have to specify Sequence directly with #[construct(/* Sequence */)] attribute.

#[derive(Construct)]
#[construct(Rect -> Node)]
pub struct Rect {
    size: (f32, f32),
}

1.4 Constructing Sequences: The Sequence for the Rect in example above becomes Rect -> Node -> Nothing. You can construct! the entire sequence within a single call:

fn create_sequence() {
    let (rect, node /* nothing */) = construct!(Rect {
        .hidden,                        // You can write just `.hidden` instead of `.hidden: true`
        .position: (10., 10.),
        .size: (10., 10.),
    });
    assert_eq!(rect.size.0, 10.);
    assert_eq!(node.position.1, 10.);
    assert_eq!(node.hidden, false);
}

1.5 Params: There are different kind of Params (the things you passing to construct!(..)):

  • Common: use Default::default() if not passed to construct!(..)
  • Default: use provided value if not passed to construct!(..)
  • Required: must be passed to construct!(..)
  • Skip: can't be passed to construct!(..), use Default::default() or provided value You configure behavior using #[param] attribute when deriving:
#[derive(Construct)]
#[construct(Follow -> Node)]
pub struct Follow {
    offset: (f32, f32),                 // Common, no #[param]

    #[param(required)]                  // Required
    target: Entity,
    
    #[param(default = Anchor::Center)]  // Default
    anchor: Anchor,

    #[param(skip)]                      // Skip with Default::default()
    last_computed_distance: f32,

    #[param(skip = FollowState::None)]  // Skip with provided value
    state: FollowState,
}

#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Entity;

pub enum Anchor {
    Left,
    Center,
    Right,
}

pub enum FollowState {
    None,
    Initialized(f32)
}

1.6 Passing params: When passing params to construct!(..) you have to pass all required for Sequence params, or you will get the compilation error. You can omit non-required params.

fn create_elements() {
    // omit everything, default param values will be used
    let (rect, node, /* nothing */) = construct!(Rect);
    assert_eq!(node.hidden, false);
    assert_eq!(rect.size.0, 0.);

    // you have to pass target to Follow, the rest can be omitted..
    let (follow, node) = construct!(Follow {
        .target: Entity
    });
    assert_eq!(follow.offset.0, 0.);
    assert_eq!(node.hidden, false);

    // ..or specified:
    let (follow, node) = construct!(Follow {
        .hidden,
        .target: Entity,
        .offset: (10., 10.),

        // last_computed_distance param is skipped, uncommenting
        // the next line will result in compilation error
        // error: no field `last_computed_distance` on type `&follow_construct::Params`
        
        // .last_computed_distance: 10.
    });
    assert_eq!(follow.offset.0, 10.);
    assert_eq!(node.hidden, true);
}

Design and Methods

2.1 Designs and Methods: Every Construct has its own Design. You can implement methods for a Construct's design:

impl NodeDesign {
    pub fn move_to(&self, entity: Entity, position: (f32, f32)) { }
}

impl RectDesign {
    pub fn expand_to(&self, entity: Entity, size: (f32, f32)) { }
}

2.2 Calling Methods: You can call methods on a Construct's design. Method resolution follows the sequence order:

fn use_design() {
    let rect_entity = Entity;
    design!(Rect).expand_to(rect_entity, (10., 10.));
    design!(Rect).move_to(rect_entity, (10., 10.)); // move_to implemented for NodeDesign
}

Segments

3.1 Segments: Segments allow you to define and insert segments into a Construct's sequence:

#[derive(Segment)]
pub struct Input {
    disabled: bool,
}

#[derive(Construct)]
#[construct(Button -> Input -> Rect)]
pub struct Button {
    pressed: bool
}

3.2 Sequence with Segments: The Sequence for Button becomes Button -> Input -> Rect -> Node -> Nothing. You can instance the entire sequence of a Construct containing segments within a single construct! call:

fn create_button() {
    let (button, input, rect, node) = construct!(Button {
        .disabled: true
    });
    assert_eq!(button.pressed, false);
    assert_eq!(input.disabled, true);
    assert_eq!(rect.size.0, 100.);
    assert_eq!(node.position.0, 0.);
}

3.3 Segment Design: Segment has its own Design as well. And the method call resolves within the Sequence order as well. Segment's designs has one generic parameter - the next segment/construct, so you have to respect it when implement Segment's Design:

impl<T> InputDesign<T> {
    fn focus(&self, entity: Entity) {
        /* do the focus stuff */
    }
}

fn focus_button() {
    let btn = Entity;
    design!(Button).focus(btn);
}

Props

4.1 Props: By deriving Constructs or Segments you also get the ability to set and get properties on items with respect of Sequence:

fn button_props() {
    let (mut button, mut input, mut rect, mut node) = construct!(Button);
    
    // You can access to props knowing only the top-level Construct
    let pos         /* Prop<Node, (f32, f32)> */    = prop!(Button.position);
    let size        /* Prop<Rect, (f32, f32)> */    = prop!(Button.size);
    let disabled    /* Prop<Input, bool> */         = prop!(Button.disabled);
    let pressed     /* Prop<Button, bool */         = prop!(Button.pressed);

    // You can read props. You have to pass exact item to the get()
    let x = pos.get(&node).as_ref().0;
    let w = size.get(&rect).as_ref().0;
    let is_disabled = *disabled.get(&input).as_ref();
    let is_pressed = *pressed.get(&button).as_ref();
    assert_eq!(0., x);
    assert_eq!(100., w);
    assert_eq!(false, is_disabled);
    assert_eq!(false, is_pressed);

    // You can set props. You have to pass exact item to set()
    pos.set(&mut node, (1., 1.));
    size.set(&mut rect, (10., 10.));
    disabled.set(&mut input, true);
    pressed.set(&mut button, true);
    assert_eq!(node.position.0, 1.);
    assert_eq!(rect.size.0, 10.);
    assert_eq!(input.disabled, true);
    assert_eq!(button.pressed, true);

}

4.2 Expand props: If you have field with Construct type, you can access this fields props as well:

#[derive(Construct, Default)]
#[construct(Vec2 -> Nothing)]
pub struct Vec2 {
    x: f32,
    y: f32,
}

#[derive(Construct)]
#[construct(Node2d -> Nothing)] 
pub struct Node2d {
    #[prop(construct)]      // You have to mark expandable props with #[prop(construct)]
    position: Vec2,
}

fn modify_position_x() {
    let mut node = construct!(Node2d);
    assert_eq!(node.position.x, 0.);
    assert_eq!(node.position.y, 0.);

    let x = prop!(Node2d.position.x);

    x.set(&mut node, 100.);
    assert_eq!(node.position.x, 100.);
    assert_eq!(node.position.y, 0.);
}

Custom Constructors

5.1 Custom Constructors: Sometimes you may want to implement Construct for a foreign type or provide a custom constructor. You can use derive_construct! for this purpose:

pub struct ProgressBar {
    min: f32,
    val: f32,
    max: f32,
}


impl ProgressBar {
    pub fn min(&self) -> f32 {
        self.min
    }
    pub fn set_min(&mut self, min: f32) {
        self.min = min;
        if self.max < min {
            self.max = min;
        }
        if self.val < min {
            self.val = min;
        }
    }
    pub fn max(&self) -> f32 {
        self.max
    }
    pub fn set_max(&mut self, max: f32) {
        self.max = max;
        if self.min > max {
            self.min = max;
        }
        if self.val > max {
            self.val = max;
        }
    }
    pub fn val(&self) -> f32 {
        self.val
    }
    pub fn set_val(&mut self, val: f32) {
        self.val = val.max(self.min).min(self.max)
    }
}

derive_construct! {
    // Sequence
    seq => ProgressBar -> Rect;

    // Constructor, all params with default values
    construct => (min: f32 = 0., max: f32 = 1., val: f32 = 0.) -> {
        if max < min {
            max = min;
        }
        val = val.min(max).max(min);
        Self { min, val, max }
    };

    // Props using getters and setters
    props => {
        min: f32 = [min, set_min];
        max: f32 = [max, set_max];
        val: f32 = [val, set_val];
    };
}

5.2 Using Custom Constructors: The provided constructor will be called when creating instances:

fn create_progress_bar() {
    let (pb, _, _) = construct!(ProgressBar { .val: 100. });
    assert_eq!(pb.min, 0.);
    assert_eq!(pb.max, 1.);
    assert_eq!(pb.val, 1.);
}

5.3 Custom Construct Props: In the example above derive_construct! declares props using getters and setters. This setters and getters are called when you use Prop::get and Prop::set

fn modify_progress_bar() {
    let (mut pb, _, _) = construct!(ProgressBar {});
    let min = prop!(ProgressBar.min);
    let val = prop!(ProgressBar.val);
    let max = prop!(ProgressBar.max);

    assert_eq!(pb.val, 0.);

    val.set(&mut pb, 2.);
    assert_eq!(pb.val, 1.0); //because default for max = 1.0

    min.set(&mut pb, 5.);
    max.set(&mut pb, 10.);
    assert_eq!(pb.min, 5.);
    assert_eq!(pb.val, 5.);
    assert_eq!(pb.max, 10.);
}

5.4 Deriving Segments: You can derive Segments in a similar way:

pub struct Range {
    min: f32,
    max: f32,
    val: f32,
}
derive_segment! {
    // use `seg` to provide type you want to derive Segment
    seg => Range;
    construct => (min: f32 = 0., max: f32 = 1., val: f32 = 0.) -> {
        if max < min {
            max = min;
    }
        val = val.min(max).max(min);
        Self { min, val, max }
    };
    // Props using fields directly
    props => {
        min: f32 = value;
        max: f32 = value;
        val: f32 = value;
    };
}

#[derive(Construct)]
#[construct(Slider -> Range -> Rect)]
pub struct Slider;

fn create_slider() {
    let (slider, range, _, _) = construct!(Slider {
        .val: 10.
    });
    assert_eq!(range.min, 0.0);
    assert_eq!(range.max, 1.0);
    assert_eq!(range.val, 1.0);
}

Limitations

  • only public structs (or enums with constructable!)
  • no generics supported yet (looks very possible)
  • limited number of params for the whole inheritance tree (default version compiles with 16, tested with 64)
  • only static structs/enums (no lifetimes)

Cost

I didn't perform any stress-tests. It should run pretty fast: there is no heap allocations, only some deref calls per construct! per defined prop per depth level. Cold compilation time grows with number of params limit (1.5 mins for 64), but the size of the binary doesn't changes.

Roadmap

  • add #![forbid(missing_docs)] to the root of each crate
  • docstring bypassing
  • generics
  • union params, so you can pas only one param from group. For example, Range could have min, max, abs and rel constructor params, and you can't pass abs and rel both.
  • nested construct inference (looks like possible):
#[derive(Construct, Default)]
pub struct Vec2 {
    x: f32,
    y: f32
}
#[derive(Construct)]
pub struct Div {
    position: Vec2,
    size: Vec2,
}
fn step_inference() {
    let div = construct!(Div {
        position: {{ x: 23., y: 20. }},
        size: {{ x: 23., y: 20. }}
    })
}

Contributing

I welcome contributions to Constructivism! If you'd like to contribute or have any questions, please feel free to open an issue or submit a pull request.

License

The constructivism is dual-licensed under either:

This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both.

Dependencies

~0.6–1.2MB
~28K SLoC