#builder #design-pattern #lazy-evaluation #struct #field #macro

fieldx_plus

Design patterns built on top of fieldx crate

3 releases

new 0.1.2 Oct 20, 2024
0.1.1 Oct 9, 2024
0.1.0 Oct 3, 2024

#861 in Rust patterns

Download history 153/week @ 2024-10-01 184/week @ 2024-10-08 152/week @ 2024-10-15

489 downloads per month

Custom license

17KB
141 lines

Workflow Status License Crates.io Version

fieldx_plus v0.1.2

This crate is intended for implementing some design patterns, based on fieldx crate. At the moment it is only Application/Agent and Parent/Child patterns. Both are basically the same thing essentially where Application/Parent is a reference counted object and agents/children hold references to it. The difference is in children accessor method names which are app and parent, correspondingly.

Unfortunately, lack of time doesn't allow me to create comprehensive documentation for this crate. Consider the tests as the starting points. But here is, briefly, an idea of how it looks:

use fieldx_plus::{fx_plus, agent_build};
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("{0}")]
    AdHoc(String),
    #[error("Application object is unexpectedly gone")]
    AppIsGone
}

#[fx_plus(app, sync)]
struct Application {
    #[fieldx(lazy, get)]
    service: NetService,
}

impl Application {
    fn build_service(&self) -> NetService {
        agent_build!(
            self, NetService {
                port: 4242,
                name: "app service",
            }
        ).unwrap()
    }

    pub fn config(&self, key: &str) -> u32 {
        42
    }

    pub fn run() {
        let app = Application::new();
        app.service().launch().expect("Something went wrong...");
    }
}

#[fx_plus(agent(Application, unwrap(error(AppError, AppError::AppIsGone))), sync)]
struct NetService {
    port: u16,
    #[fieldx(into, get)]
    name: String,
}

impl NetService {
    pub fn launch(&self) -> Result<(), AppError> {
        let app = self.app()?;
        let cfg = app.config("foo");
        println!("Launching '{}' service.", self.name());
        Ok(())
    }
}

fn main() {
    Application::run();
}

Here is a quick breakdown for it:

fx_plus is an extender to fieldx::fxstruct attribute. As such, it takes all the arguments, fxstruct take and adds of couple of its own. But be aware that it overrides some of fxstruct argument:

  • with app or parent fxstruct(rc) is enforced
  • with agent or child it sets fxstruct(no_new, builder); setting additional parameters with the builder are allowed, though: fx_plus(child, builder(into))

With any of the four serde gets disabled if serde feature is enabled.

Since application/parent struct is always rc there is a chance that its reference counter can drop to 0 while an agent/child with weak reference attempts to call .app() or .parent(), which internally attempt to upgrade the reference. agent(unwrap) argument controls how this situation is handled: arguments:

  • child(unwrap) results in simple .unwrap() applied the upgrade call
  • unwrap(expect("Error message") commands to use .expect("Error message"); with this and the above variant we always get just the app/parent object
  • unwrap(error(ErrorType, <expression>)) would produce app/parent methods that return ErrorType; the particular value returned depends on the <expression>. Say, ErrorType::ParentIsGone can be used to return a specific error code
  • unwrap(map(ErrorType, <expr>)) can be used to invoke a method on self. <expr> can either be just method name or something like map_to_err("argument", 42) in which case the map_to_err method will get the arguments specified.

Helper macros agent_build, agent_builder, child_build, and child_builder are wrappers around builder pattern. I.e. agent_build!(self.app(), Agent { foo: 42, bar: "baz" }) is actually a shortcut for Agent::builder().app(self.app()).foo(42).bar("baz").build(). agent_builder is the same but without the final build() call.

License

Licensed under the BSD 3-Clause License.

Dependencies

~1.3–6.5MB
~42K SLoC