Skip to content

Commit

Permalink
Add some lifecycle callbacks to Tinky::Object
Browse files Browse the repository at this point in the history
This is for the benefit of implementations that may need to persist the
state to some storage.

Also fix the implementation of the workflow specific role.
  • Loading branch information
jonathanstowe committed Apr 25, 2021
1 parent 9e04348 commit 9a67e7d
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 19 deletions.
3 changes: 3 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
v0.1.2 Mon 19 Apr 14:44:33 BST 2021
* Add some lifecycle callbacks to the object for the convenience of sub-classes
* Fix the application of the role
v0.1.1 Mon 25 Jan 13:06:26 GMT 2021
* Some changes to make it easier for sub-classes
v0.1.0 Mon 18 Jan 10:15:01 GMT 2021
Expand Down
26 changes: 25 additions & 1 deletion Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,9 @@ The same rules for execution based on the signature and the object to which the
role Tinky::Object
------------------

This is a role that should should be applied to any application object that is to have a state managed by [Tinky::Workflow](Tinky::Workflow), it provides the mechanisms for transition application and allows the transitions to be validated by the mechanisms described above for [Tinky::State](Tinky::State) and [Tinky::Transition](Tinky::Transition)
This is a role that should should be applied to any application object that is to have a state managed by [Tinky::Workflow](Tinky::Workflow), it provides the mechanisms for transition application and allows the transitions to be validated by the mechanisms described above for [Tinky::State](Tinky::State) and [Tinky::Transition](Tinky::Transition).

There are also method traits to define methods that will be called before and after the application of the workflow to the object, and before and after the application of a transition to the object.

### method state

Expand Down Expand Up @@ -440,6 +442,28 @@ This returns the transition that would place the object in the supplied state fr

Used to smart match the object against either a State (returns True if the state matches the current state of the object,) or a Transition (returns True if the `from` state matches the current state of the object.)

### trait before-apply-workflow

This trait can be applied to a method that will be called within the `apply-workflow` immediately after the application has been validated and before anything else occurs (primarily the setting of an initial state state if any.) The method will be called with the Workflow object as an argument.

This is primarily for the benefit of implementations that may want to, for instance, retrieve the object state from some storage before setting up the rest of the workflow.

### trait after-apply-workflow

This trait can be applied to a method that will be called within the `apply-workflow` immediately before the Workflow's `applied` method is called with the object, that is the initial state will have been set and other changes made. The method will be called with the Workflow object as an argument.

This may be helpful, for example, if one wanted to persist the initial state to some storage.

### trait before-apply-transition

This trait can be applied to a method that will be called within the `apply-transition` immediately after the validation has been done and before the state is actually changed. The method will be called with the Transition object as the argument.

### trait after-apply-transition

This trait can be applied to a method that will be called within the `apply-transition` after the state of the object has been changed and immediately before the object is passed to the transition's `applied` method. The method will be called with the Transition object as the argument.

This might be used, for example, to persist the change of state of the object to some storage.

EXCEPTIONS
----------

Expand Down
2 changes: 1 addition & 1 deletion META6.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Tinky",
"version": "0.1.1",
"version": "0.1.2",
"auth": "github:jonathanstowe",
"api": "1.0",
"description": "An Experimental Workflow / State Management toolit",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ An experimental workflow module for Raku

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

```perl6
```raku

use Tinky;

Expand Down
138 changes: 125 additions & 13 deletions lib/Tinky.pm
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,11 @@ This is a role that should should be applied to any application object
that is to have a state managed by L<Tinky::Workflow>, it provides the
mechanisms for transition application and allows the transitions to
be validated by the mechanisms described above for L<Tinky::State>
and L<Tinky::Transition>
and L<Tinky::Transition>.
There are also method traits to define methods that will be called before
and after the application of the workflow to the object, and before and
after the application of a transition to the object.
=head3 method state
Expand Down Expand Up @@ -662,6 +666,47 @@ the state matches the current state of the object,) or a Transition
(returns True if the C<from> state matches the current state of the
object.)
=head3 trait before-apply-workflow
This trait can be applied to a method that will be called within the
C<apply-workflow> immediately after the application has been validated
and before anything else occurs (primarily the setting of an initial
state state if any.) The method will be called with the Workflow
object as an argument.
This is primarily for the benefit of implementations that may want
to, for instance, retrieve the object state from some storage before
setting up the rest of the workflow.
=head3 trait after-apply-workflow
This trait can be applied to a method that will be called within the
C<apply-workflow> immediately before the Workflow's C<applied> method
is called with the object, that is the initial state will have been
set and other changes made. The method will be called with the Workflow
object as an argument.
This may be helpful, for example, if one wanted to persist the initial
state to some storage.
=head3 trait before-apply-transition
This trait can be applied to a method that will be called within the
C<apply-transition> immediately after the validation has been done and
before the state is actually changed. The method will be called with
the Transition object as the argument.
=head3 trait after-apply-transition
This trait can be applied to a method that will be called within the
C<apply-transition> after the state of the object has been changed and
immediately before the object is passed to the transition's C<applied>
method. The method will be called with the Transition object as the
argument.
This might be used, for example, to persist the change of state of the
object to some storage.
=head2 EXCEPTIONS
The methods for applying a transition to an object will signal an
Expand Down Expand Up @@ -727,7 +772,7 @@ current state on the object.
=end pod

module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
module Tinky:ver<0.1.2>:auth<github:jonathanstowe>:api<1.0> {

# Stub here, definition below
class State { ... };
Expand All @@ -739,7 +784,7 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
# This is a handy type definition to save having to type
# this out all over the place so we can pretend a role
# is a type.
subset Role of Mu where { $_.HOW.archetypes.composable };
subset Role of Mu where { $_.DEFINITE && $_.HOW.archetypes.composable };

# Traits for user defined state and transition classes
# The roles are only used to indicate the purpose of the
Expand Down Expand Up @@ -793,6 +838,42 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
@meths;
}

# This is a base role for the Object apply-workflow callbacks
role ObjectWorkflowPhase {
}

# indicate the 'before-apply-workflow' callbacks for a Tinky::Object
role ObjectWorkflowBefore does ObjectWorkflowPhase {
}
multi sub trait_mod:<is> (Method $m, :$before-apply-workflow! ) is export {
$m does ObjectWorkflowBefore;
}

# indicate the 'after-apply-workflow' callbacks for a Tinky::Object
role ObjectWorkflowAfter does ObjectWorkflowPhase {
}
multi sub trait_mod:<is> (Method $m, :$after-apply-workflow! ) is export {
$m does ObjectWorkflowAfter;
}

# This is a base role for the Object apply-transition callbacks
role ObjectTransitionPhase {
}

# indicate the 'before-apply-transition' callbacks for a Tinky::Object
role ObjectTransitionBefore does ObjectTransitionPhase {
}
multi sub trait_mod:<is> (Method $m, :$before-apply-transition! ) is export {
$m does ObjectTransitionBefore;
}

# indicate the 'after-apply-transition' callbacks for a Tinky::Object
role ObjectTransitionAfter does ObjectTransitionPhase {
}
multi sub trait_mod:<is> (Method $m, :$after-apply-transition! ) is export {
$m does ObjectTransitionAfter;
}

class Tinky::X::Fail is Exception {
}

Expand Down Expand Up @@ -977,7 +1058,7 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
validate-helper($object, ( @!validators, validate-methods(self, $object, ApplyValidator)).flat);
}

has $!role;
has Mu $.role;

method states() {
if not @!states.elems {
Expand Down Expand Up @@ -1042,9 +1123,9 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
self.transitions-for-state($from).first({ $_.to ~~ $to });
}

method role( --> Role ) {
method role( --> Mu ) {
if not $!role ~~ Role {
$!role = role { };
$!role := Metamodel::ParametricRoleHOW.new_type(name => self.^name ~ '::' ~ ( self.name // 'role' ) );
for @.transitions.classify(-> $t { $t.name }).kv -> $name, $transitions {
my $method = method (|c) {
if $transitions.grep(self.state).head -> $tran {
Expand All @@ -1054,8 +1135,11 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
Tinky::X::InvalidTransition.new(message => "No transition '$name' for state '{ self.state.Str }'").throw;
}
}
$method.set_name($name);
$!role.^add_method($name, $method);
}
$!role.^set_body_block( -> |c { });
$!role.^compose;
}
$!role;
}
Expand All @@ -1080,11 +1164,13 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
$SELF!state = $val;
}
else {
if $SELF.transition-for-state($val) -> $trans {
$SELF.apply-transition($trans);
}
else {
Tinky::X::NoTransition.new(from => $SELF.state, to => $val).throw;
if $val !~~ $SELF!state {
if $SELF.transition-for-state($val) -> $trans {
$SELF.apply-transition($trans);
}
else {
Tinky::X::NoTransition.new(from => $SELF.state, to => $val).throw;
}
}
}
$SELF!state;
Expand All @@ -1095,12 +1181,14 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
method apply-workflow(Workflow $wf) {
my $p = $wf.validate-apply(self);
if $p.result {
self.before-apply-workflow($wf);
$!workflow = $wf;
if not $!state.defined and $!workflow.initial-state.defined {
$!state = $!workflow.initial-state;
}
try self does $wf.role;
$wf.applied(self);;
self.^mixin($wf.role);
self.after-apply-workflow($wf);
$wf.applied(self);
}
else {
Tinky::X::ObjectRejected.new(workflow => $wf).throw;
Expand Down Expand Up @@ -1146,7 +1234,9 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
if $!state.defined {
if self ~~ $trans {
if await $trans.validate-apply(self) {
self.before-apply-transition($trans);
$!state = $trans.to;
self.after-apply-transition($trans);
$trans.applied(self);
$!state;
}
Expand All @@ -1168,6 +1258,28 @@ module Tinky:ver<0.1.1>:auth<github:jonathanstowe>:api<1.0> {
Tinky::X::NoState.new.throw;
}
}

method before-apply-workflow(Workflow $workflow --> Nil) {
for self.^methods.grep(ObjectWorkflowBefore) -> $method {
self.$method($workflow);
}
}
method after-apply-workflow(Workflow $workflow --> Nil) {
for self.^methods.grep(ObjectWorkflowAfter) -> $method {
self.$method($workflow);
}
}

method before-apply-transition(Transition $transition --> Nil) {
for self.^methods.grep(ObjectTransitionBefore) -> $method {
self.$method($transition);
}
}
method after-apply-transition(Transition $transition --> Nil) {
for self.^methods.grep(ObjectTransitionAfter) -> $method {
self.$method($transition);
}
}
}
}
# vim: expandtab shiftwidth=4 ft=raku
60 changes: 57 additions & 3 deletions t/040-workflow-object.t
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ throws-like { Tinky::Workflow.new.states }, Tinky::X::NoTransitions, ".states th

my Tinky::Workflow $wf;

lives-ok { $wf = Tinky::Workflow.new(:@transitions) }, "create new workflow with transitions";
lives-ok { $wf = Tinky::Workflow.new(name => 'first-test-workflow', :@transitions) }, "create new workflow with transitions";
is $wf.transitions.elems, @transitions.elems, "and got the right number of transitions";
is $wf.states.elems, @states.elems, "and calculated the right number of states";

Expand Down Expand Up @@ -64,8 +64,8 @@ subtest {
my $wf = Tinky::Workflow.new(:@transitions, initial-state => @states[0]);
ok $wf.initial-state ~~ @states[0], "just check the initial-state got set";
my $obj = FooTest.new();
#lives-ok {
$obj.apply-workflow($wf); # }, "apply workflow with an initial-state (object has no state)";
lives-ok {
$obj.apply-workflow($wf); }, "apply workflow with an initial-state (object has no state)";
ok $obj.state ~~ @states[0], "and the new object now has that state";
my $new-state = Tinky::State.new(name => 'new-state');
$obj = FooTest.new(state => $new-state);
Expand All @@ -75,5 +75,59 @@ subtest {

}, "initial state on Workflow";

subtest {
my class BarTest does Tinky::Object {
has Str $.before;
has Str $.after;

method before-method(Tinky::Workflow $wf) is before-apply-workflow {
$!before = $wf.name;
}
method after-method(Tinky::Workflow $wf) is after-apply-workflow {
$!after = $wf.name;
}
}

my $wf = Tinky::Workflow.new(name => 'apply-callback-test-workflow', :@transitions, initial-state => @states[0]);

my $object = BarTest.new;

lives-ok { $object.apply-workflow($wf) }, 'apply-workflow with apply callbacks';

is $object.before, $wf.name, "saw the before";
is $object.after, $wf.name, "saw the after";



}, "apply-workflow callbacks";

subtest {
my class ZubTest does Tinky::Object {
has Str $.before;
has Str $.after;

method before-method(Tinky::Transition $trans) is before-apply-transition {
$!before = $trans.name;
}
method after-method(Tinky::Transition $trans) is after-apply-transition {
$!after = $trans.name;
}
}

my $wf = Tinky::Workflow.new(name => 'apply-test-transition-workflow', :@transitions, initial-state => @states[0]);

my $object = ZubTest.new;

lives-ok { $object.apply-workflow($wf) }, 'apply-workflow';

lives-ok { $object.state = @states[1]; }, "apply a transition";

is $object.before, 'one-two', "saw the before";
is $object.after, 'one-two', "saw the after";



}, "apply-transition callbacks";

done-testing;
# vim: expandtab shiftwidth=4 ft=raku

0 comments on commit 9a67e7d

Please sign in to comment.