### ----------------------------------------------------
### -- Tinky
### -- Licenses: Artistic-2.0
### -- Authors: Jonathan Stowe <jns+git@gellyfish.co.uk>
### -- File: README.md
### ----------------------------------------------------
# Tinky

An experimental workflow module for Raku

![Build Status](https://github.com/jonathanstowe/Tinky/workflows/CI/badge.svg)

## Synopsis

This is quite long [skip to the Description](#description) if you are impatient.

```raku

use Tinky;


# A class that will use the workflow
# it can have any attributes or methods
# required by your application

class Ticket does Tinky::Object {
    has Str $.ticket-number = (^100000).pick.fmt("%08d");
    has Str $.owner;
}

# Set up some states as required by the application
my $state-new         = Tinky::State.new(name => 'new');
my $state-open        = Tinky::State.new(name => 'open');
my $state-rejected    = Tinky::State.new(name => 'rejected');
my $state-in-progress = Tinky::State.new(name => 'in-progress');
my $state-stalled     = Tinky::State.new(name => 'stalled');
my $state-complete    = Tinky::State.new(name => 'complete');

# Each state has an 'enter-supply' and a 'leave-supply' which get the
# object which the state was applied to.

$state-rejected.enter-supply.act( -> $object { say "** sending rejected e-mail for Ticket '{ $object.ticket-number }' **"});

# Create some transitions to describe pre-determined change of state
# A method will be created on the Tinky::Object for each transition name

my $open              = Tinky::Transition.new(name => 'open', from => $state-new, to => $state-open);

# Where  more than one transition has the same name, the transition which matches the object's 
# current state will be use.
my $reject-new        = Tinky::Transition.new(name => 'reject', from => $state-new, to => $state-rejected);
my $reject-open       = Tinky::Transition.new(name => 'reject', from => $state-open, to => $state-rejected);
my $reject-stalled    = Tinky::Transition.new(name => 'reject', from => $state-stalled, to => $state-rejected);

my $stall-open        = Tinky::Transition.new(name => 'stall', from => $state-open, to => $state-stalled);
my $stall-progress    = Tinky::Transition.new(name => 'stall', from => $state-in-progress, to => $state-stalled);

# The transition supply allows specific logic for the transition to be performed

$stall-progress.supply.act( -> $object { say "** rescheduling tickets for '{ $object.owner }' on ticket stall **"});

my $unstall           = Tinky::Transition.new(name => 'unstall', from => $state-stalled, to => $state-in-progress);

my $take              = Tinky::Transition.new(name => 'take', from => $state-open, to => $state-in-progress);

my $complete-open     = Tinky::Transition.new(name => 'complete', from => $state-open, to => $state-complete);
my $complete-progress = Tinky::Transition.new(name => 'complete', from => $state-in-progress, to => $state-complete);

my @transitions = $open, $reject-new, $reject-open, $reject-stalled, $stall-open, $stall-progress, $unstall, $take, $complete-open, $complete-progress;

# The Workflow object allows the relation between states and transitions to be calculate
# and generates the methods that will be applied to the ticket object.  The initial-state
# will be applied to the object if there is no existing state on the state.
my $workflow = Tinky::Workflow.new(:@transitions, name => 'ticket-workflow', initial-state => $state-new );

# The workflow aggregates the Supplies of the transitions and the states.
# This could be to a logging subsystem for instance. 

$workflow.transition-supply.act(-> ($trans, $object) { say "Ticket '{ $object.ticket-number }' went from { $trans.from.name }' to '{ $trans.to.name }'" });

# The final-supply emits the state and the object when a state is reached where there are no
# further transitions available

$workflow.final-supply.act(-> ( $state, $object) { say "** updating performance stats with Ticket '{ $object.ticket-number }' entered State '{ $state.name }'" });

# Create an instance of the Tinky::Object.
# A 'state' can be supplied to initialise if, for example, the data was retrieved from a database.
my $ticket-a = Ticket.new(owner => "Operator A");

# Applying the workflow will set the initial state if one is configured and will
# apply a role that provides the transition methods.
# The workflow object can be configured to check whether the object to which it
# is being applied is suitable and throw an exception if not.

$ticket-a.apply-workflow($workflow);

# Exercise the transition methods.
# Other mechanisms are available for performing the transitions whuch may be more
# suitable if the next state is to be calculated.

# State new -> open
$ticket-a.open;

# State open -> in-progress
$ticket-a.take;

# Get the names of the states which are now available for the object
# [stalled complete]
$ticket-a.next-states>>.name.say;

# Directly assigning the state will be validated, an exception will
# be thrown if this is not a valid transition at the time
$ticket-a.state = $state-stalled;

# State stalled -> rejected
# This is a final state and no further transitions are available.
$ticket-a.reject;

```

## Description

Tinky is a deterministic state manager that can be used to implement a
workflow system, it provides a role ```Tinky::Object``` that allows an
object to have a managed state.

A ```Workflow``` is simply a set of ```State```s and allowable transitions
between them. Validators can be defined to check whether an object should
be allowed to enter or leave a specific state or have a transition
performed, asynchronous notification of state change (enter, leave or
transition application,) is provided by Supplies which are available at
```State```/```Transition``` level or aggregrated.

I have taken somewhat of a "kitchen-sink" toolkit approach with this to
provide a somewhat rich interface that can be more easily used to create
higher level applications which interact nicely with Raku's reactive
and composable nature. I've taken some ideas from similar software in
other languages that I have used and added features that I would have
liked for the problems that I was solving and ended up providing myself.

By the way I've deliberately chosen a "non-enterprisey" name for this
for a couple of reasons, largely however because I'm not convinced that
this will remain the gold standard within the problem domain I don't
want to block out a more sensible name for someone who may want to make
something later, also the possible candidates involving some variant
of "Workflow" or "state machine" don't properly fit what I see this as
which is something that has some of the features of the latter in order
to provide a toolkit to help implement the former.

The full documentation is available [here](Documentation.md).

## Installation

Assuming you have a working Rakudo installation you should be able to install this with *zef* :

    # From the source directory
   
    zef install .

    # Remote installation

    zef install Tinky

## Support

This is an experimental software but suggestions and patches that
may make it more useful in your software are welcomed via github at

https://github.com/jonathanstowe/Tinky/issues


## Licence

This is free software.

Please see the [LICENCE](LICENCE) file in the distribution.

© Jonathan Stowe 2016 - 2021


