diff --git a/Changes b/Changes index 20a3fcc..d4a111c 100644 --- a/Changes +++ b/Changes @@ -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 diff --git a/Documentation.md b/Documentation.md index b9c49da..7709008 100644 --- a/Documentation.md +++ b/Documentation.md @@ -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 @@ -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 ---------- diff --git a/META6.json b/META6.json index a1a968d..289cefb 100644 --- a/META6.json +++ b/META6.json @@ -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", diff --git a/README.md b/README.md index 263f31a..43ac327 100644 --- a/README.md +++ b/README.md @@ -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; diff --git a/lib/Tinky.pm b/lib/Tinky.pm index 93f669e..09cd1e5 100644 --- a/lib/Tinky.pm +++ b/lib/Tinky.pm @@ -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, it provides the mechanisms for transition application and allows the transitions to be validated by the mechanisms described above for L -and L +and L. + +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 @@ -662,6 +666,47 @@ the state matches the current state of the object,) or a Transition (returns True if the C 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 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 immediately before the Workflow's C 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 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 after the state of the object has been changed and +immediately before the object is passed to the transition's C +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 @@ -727,7 +772,7 @@ current state on the object. =end pod -module Tinky:ver<0.1.1>:auth:api<1.0> { +module Tinky:ver<0.1.2>:auth:api<1.0> { # Stub here, definition below class State { ... }; @@ -739,7 +784,7 @@ module Tinky:ver<0.1.1>:auth: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 @@ -793,6 +838,42 @@ module Tinky:ver<0.1.1>:auth: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: (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: (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: (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: (Method $m, :$after-apply-transition! ) is export { + $m does ObjectTransitionAfter; + } + class Tinky::X::Fail is Exception { } @@ -977,7 +1058,7 @@ module Tinky:ver<0.1.1>:auth:api<1.0> { validate-helper($object, ( @!validators, validate-methods(self, $object, ApplyValidator)).flat); } - has $!role; + has Mu $.role; method states() { if not @!states.elems { @@ -1042,9 +1123,9 @@ module Tinky:ver<0.1.1>:auth: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 { @@ -1054,8 +1135,11 @@ module Tinky:ver<0.1.1>:auth: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; } @@ -1080,11 +1164,13 @@ module Tinky:ver<0.1.1>:auth: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; @@ -1095,12 +1181,14 @@ module Tinky:ver<0.1.1>:auth: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; @@ -1146,7 +1234,9 @@ module Tinky:ver<0.1.1>:auth: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; } @@ -1168,6 +1258,28 @@ module Tinky:ver<0.1.1>:auth: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 diff --git a/t/040-workflow-object.t b/t/040-workflow-object.t index 84236af..956b7ec 100644 --- a/t/040-workflow-object.t +++ b/t/040-workflow-object.t @@ -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"; @@ -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); @@ -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