Type-safe process modelling library
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.
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.
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.
- 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!
- 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 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
The following flowchart illustrates an example process:
flowchart TD
ShortcodeStringEntry --> SelectAmountSource{{SelectAmountSource}}
SelectAmountSource -->|PredefinedAmount| DisplayAmount
SelectAmountSource -->|CustomAmount| AmountForm
AmountForm --> DisplayAmount
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 runsequenceDiagram
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
%%{
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"
Africa's Talking API Reference
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 xtaskSome integration tests require Docker to be running on your machine to start containers for external dependencies (e.g., Postgres).