Skip to content

Only1Loatheb/brrricks

Repository files navigation

Brrricks

Type-safe process modelling library

Core concepts

Step is a unit of execution. Each step belongs to one of the following archetypes: Entry, Operation, Form, Splitter, FormSplitter, or Final.

Process is a composition of steps with a defined execution order, including conditional branches and early termination paths.

Parameter (param) is a value produced by a step and carried forward across subsequent steps within the same process execution. Same process execution can span across multiple user interactions.

Project goals

Process implemented with this library has the following invariants enforced at compile-time:

  • each step may only consume parameters that are guaranteed to be produced earlier in the process,
  • all execution paths must terminate in a final step,
  • every branch introduced by a split step must have a corresponding continuation defined,
  • once a parameter is produced in every execution path, it cannot be overwritten in subsequent steps. If a parameter value is present only in a subset of paths, it is removed from the session context to guarantee that downstream steps operate only on parameters that are present in all incoming paths. Removing the parameter from the session context allows reusing it in later in the process, but parameter store implementation is required to remove the parameter from cache if it is removed from the session context.

Simple explanation

Imagine you're creating a Choose Your Own Adventure book:

“If you pick A, go to page 5. If you pick B, go to page 10.”

This library is like a guide that helps you build that kind of story in a clear and structured way.
It helps you organize the steps into a coherent story.
Thanks to it, you can be confident that your game is logical, complete, and free from certain kinds of issues.

How is the story built?

  • Create a proper introduction. It will be an entry point to the story.
  • Continue the story using various question forms and action operations.
  • Introduce a form splitter to let the reader choose a path, go left or right.
  • Face the reader with consequences of their previous actions.
    Decide the story continuation path with a splitter.
  • And at the end, there must always be a proper finale!

What does the tool take care of?

  • Nothing appears "out of nowhere". The reader must collect an item or witness something before it is required later in the story.
  • Every choice leads somewhere, there are no missing pages.
  • Every story always has a proper ending.
  • It’s always clear where the reader left off.
  • The story can include branching paths and decisions.
  • The story won’t require the reader to collect the same item twice unless it is possible to reach that point in the story without it.

What still needs to be decided?

  • What the interaction looks like (what the reader sees and what they respond with)
  • What information you want to remember about the user and the system across the interactions (session context)
  • Where that information is stored between the interactions

Code example

The following flowchart illustrates an example process:

flowchart TD
    ShortcodeStringEntry --> SelectAmountSource{{SelectAmountSource}}
    SelectAmountSource -->|PredefinedAmount| DisplayAmount
    SelectAmountSource -->|CustomAmount| AmountForm
    AmountForm --> DisplayAmount
Loading

The process shown in the flowchart can be implemented using Brrricks:

mod standard_io_process_runner;

use crate::standard_io_process_runner::{Message, Messages, standard_io_process_runner};
use frunk_core::hlist::HNil;
use frunk_core::{Coprod, HList, hlist, hlist_pat};
use serde::{Deserialize, Serialize};
use type_process_builder::builder::*;
use type_process_builder::step::{Entry, FailedInputValidationAttempts, Final, Form, FormSplitter, InputValidation};
use typenum::*;

#[derive(Clone, Deserialize, Serialize)]
struct ShortcodeString(String);
impl ParamValue for ShortcodeString {
  type UID = U0;
}

#[derive(Clone, Deserialize, Serialize)]
struct Amount(u32);
impl ParamValue for Amount {
  type UID = U1;
}

struct ShortcodeStringEntry;
impl Entry for ShortcodeStringEntry {
  type Produces = HList![ShortcodeString];
  type Messages = Messages;

  async fn handle(
    &self,
    _consumes: Vec<(ParamUID, Vec<u8>)>,
    shortcode_string: String,
  ) -> anyhow::Result<HList![ShortcodeString]> {
    Ok(hlist!(ShortcodeString(shortcode_string)))
  }
}

pub struct PredefinedAmount;
pub struct CustomAmount;
struct SelectAmountSource;
impl FormSplitter for SelectAmountSource {
  type CreateFormConsumes = HNil;
  type ValidateInputConsumes = HNil;
  type Produces = Coprod![(PredefinedAmount, HList![Amount]), (CustomAmount, HNil)];
  type Messages = Messages;

  async fn create_form(&self, _consumes: Self::CreateFormConsumes) -> anyhow::Result<Message> {
    Ok(Message("Enter 1 for 100 or 2 for custom amount ".into()))
  }

  async fn handle_input(
    &self,
    _consumes: Self::ValidateInputConsumes,
    user_input: String,
    _failed_input_validation_attempts: FailedInputValidationAttempts,
  ) -> anyhow::Result<InputValidation<Self::Produces, Messages>> {
    Ok(match user_input.as_str() {
      "1" => InputValidation::Successful(Self::Produces::inject((PredefinedAmount, hlist!(Amount(100))))),
      "2" => InputValidation::Successful(Self::Produces::inject((CustomAmount, HNil))),
      _ => InputValidation::Retry(Message("not 1 or 2".into())),
    })
  }
}

struct AmountForm;
impl Form for AmountForm {
  type CreateFormConsumes = HNil;
  type ValidateInputConsumes = HNil;
  type Produces = HList![Amount];
  type Messages = Messages;

  async fn create_form(&self, _consumes: Self::CreateFormConsumes) -> anyhow::Result<Message> {
    Ok(Message("Enter a number".into()))
  }

  async fn handle_input(
    &self,
    _consumes: Self::ValidateInputConsumes,
    user_input: String,
    _failed_input_validation_attempts: FailedInputValidationAttempts,
  ) -> anyhow::Result<InputValidation<Self::Produces, Messages>> {
    match user_input.parse::<u32>() {
      Ok(value) => Ok(InputValidation::Successful(hlist![Amount(value)])),
      Err(_) => Ok(InputValidation::Retry(Message("Invalid number".into()))),
    }
  }
}

struct DisplayAmount;
impl Final for DisplayAmount {
  type Consumes = HList![ShortcodeString, Amount];
  type FinalMessage = Message;

  async fn handle(&self, consumes: Self::Consumes) -> anyhow::Result<Message> {
    let hlist_pat!(_shortcode_string, amount) = consumes;
    Ok(Message(format!("The amount was: {}. Good bye!", amount.0)))
  }
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
  let process = ShortcodeStringEntry
    .show_split(SelectAmountSource)
    .case_via(PredefinedAmount, |x| x)
    .case_via(CustomAmount, |x| x.show(AmountForm))
    .end(DisplayAmount)
    .build("demo_process", 0);
  standard_io_process_runner(process).await
}

To run the example process in your terminal, execute the following command:

cargo run

Typical USSD service interaction flow

sequenceDiagram
    actor User
    participant Platform
    box Purple
        participant App
    end
    participant SessionStore
    User ->> Platform: Dial *123 #35;
    Platform ->> App: /session/new
    App ->> App: Process initial request
    App ->> SessionStore: Store session
    SessionStore --) App: Session stored
    App --) Platform: First USSD screen
    Platform --) User: Display USSD screen
    User ->> Platform: Input value
    Platform ->> App: /session/continue
    App ->> SessionStore: Fetch session data
    SessionStore --) App: Session data
    App ->> App: Process input
    App ->> SessionStore: Update session
    SessionStore --) App: Session updated
    App --) Platform: Input USSD screen
    Platform --) User: Display USSD screen
    User ->> Platform: Input value
    Platform ->> App: /session/continue
    App ->> SessionStore: Fetch session data
    SessionStore --) App: Session data
    App ->> App: Process input
    App ->> SessionStore: Delete session data
    SessionStore --) App: Session deleted
    App --) Platform: Final USSD screen
    Platform --) User: Display USSD screen
Loading

Process builder states

%%{
  init: {
    'flowchart': {
      'defaultRenderer': 'tidy-tree'
    },
    'themeVariables': {
      'edgeLabelBackground': '#000000'
    }
  }
}%%
flowchart TD
    classDef default fill: transparent;
    classDef hidden display: none;
    classDef orangeNodeEdge stroke: orange;
    classDef noEdge stroke: transparent, fill: black;
    Start:::hidden
    FinalizedSplitProcessSubgraph:::hidden
    subgraph FinalizedSplitProcessSubgraph
        FinalizedSplitProcess(Finalized Split Process)
        finalized_split_cases_final{are split cases<br>exhausted?}:::orangeNodeEdge
    end
    FlowingSplitProcessSubgraph:::hidden
    subgraph FlowingSplitProcessSubgraph
        FlowingSplitProcess(Flowing Split Process)
        flowing_split_cases{are split cases<br>exhausted?}:::orangeNodeEdge
    end
    FinalizedProcess(Finalized Process) -- " build " --> RunnableProcess(Runnable Process)
    Start -- " Entry Step " --> FlowingProcess(Flowing Process)
    FlowingProcess -- " Final Step " --> FinalizedProcess
    FlowingProcess -- " Splitter Step<br>or Form Splitter Step " --> FinalizedSplitProcess
    Loop:::noEdge
    Loop(" Operation Step<br>or Form Step ") --- FlowingProcess
    Loop --> FlowingProcess
    FinalizedSplitProcess -- " Flowing Process " --> FlowingSplitProcess
    FinalizedSplitProcess -- " Finalized Process " --> finalized_split_cases_final
    finalized_split_cases_final -- " unhandled cases left " --> FinalizedSplitProcess
    finalized_split_cases_final -- " all cases addressed " --> FinalizedProcess
    FlowingSplitProcess -- " Finalized Process<br>or Flowing Process " --> flowing_split_cases
    flowing_split_cases -- " unhandled cases left " --> FlowingSplitProcess
    flowing_split_cases -- " all cases addressed " --> FlowingProcess
    Start ~~~ Loop
    click FlowingSplitProcess "https://github.com/Only1Loatheb/brrricks/blob/master/type_process_builder/src/builder/flowing_split_process.rs"
    click FlowingProcess "https://github.com/Only1Loatheb/brrricks/blob/master/type_process_builder/src/builder/flowing_process.rs"
    click FinalizedSplitProcess "https://github.com/Only1Loatheb/brrricks/blob/master/type_process_builder/src/builder/finalized_split_process.rs"
    click FinalizedProcess "https://github.com/Only1Loatheb/brrricks/blob/master/type_process_builder/src/builder/finalized_process.rs"
    click RunnableProcess "https://github.com/Only1Loatheb/brrricks/blob/master/type_process_builder/src/builder/runnable_process.rs"
Loading

Plausible use cases

Africa's Talking API Reference

Creditswitch API Reference

Qrios API Reference

Development setup

The following xtask installs monk to handle git hooks and updates autogenerated files.

To run xtask in your terminal, execute the following command in the repository root directory:

cargo xtask

Some integration tests require Docker to be running on your machine to start containers for external dependencies (e.g., Postgres).

About

Type-safe process modelling library

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors