The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package FSM::Tiny;

# http://www.crsr.net/Programming_Languages/PerlAutomata.html
use strict;
use warnings;

our $VERSION = '0.01';
our $DEBUG = 0;

use Class::Accessor::Lite;

my %Defaults = (
    current  => '',
    (map { $_ => {} } qw/rules context/),
    (map { $_ => sub {} } qw/on_enter on_exit on_transition/)
);

Class::Accessor::Lite->mk_accessors(keys %Defaults);

sub new {
    my $package = shift;
    my %args = $_[1] ? %{ @_ } : %{ $_[0] };
    my $self = bless +{ %Defaults, %args }, $package;

    for my $key (keys %{ $self->rules }) {
        my $s = $self->rules->{$key};
        if (my $r = ref $s) {
            if ($r eq 'ARRAY') {
                $self->register($key, @$s);
            }
            elsif ($r eq 'CODE') {
                $self->register($key, $s);
            }
        }
        else {
            delete $self->rules->{$key};
        }
    }

    return $self;
}

sub _log { warn "[FSM::Simele DEBUG] ".join(' ', @_) . "\n" if $DEBUG }

sub register {
    my $self = shift;
    my ($key, $code, $guards) = @_;
    $self->current($key) unless $self->current;
    $guards ||= [];
    _log("register: ${key}");
    $self->rules->{$key} = FSM::Tiny::State->new(
        code   => $code,
        guards => $guards
    );
}

sub unregister {
    my ($self, $key) = @_;
    _log("unregister: ${key}");
    delete $self->rules->{$key};
}

sub step {
    my $self = shift;
    my $st = $self->rules->{$self->current} or return;
    $st->run($self->context);
    $self->current($st->next($self->context));
    _log("next -> " . $self->current);
    return 1;
}

sub run {
    my $self = shift;
    $self->context(+{ %{ $self->context }, %{ $_[0] || {} } });
    local $_ = $self->context;
    $self->on_enter->($self->context);
    while (1) {
        if (!@{$self->rules->{$self->current}{guards}}) {
            $self->step;
            last;
        }
        $self->step or last;
        $self->on_transition->($self->context);
    }
    $self->on_exit->($self->context);
    $self;
}

package FSM::Tiny::State;

sub new {
    my $package = shift;
    my %args = @_;
    my @guards = @{ $args{guards} || [] };
    my @list;
    while (@guards) {
        my ($key, $code) = splice @guards, 0, 2;
        push @list, FSM::Tiny::Guard->new(
            key  => $key,
            code => (ref($code) || '') ne 'CODE' ? sub { $code } : $code
        );
    }
    $args{guards} = \@list;
    bless \%args, $package;
}

sub next {
    my ($self, $context) = @_;
    for my $guard (@{ $self->{guards} }) {
        return $guard->key if $guard->check($context);
    }
    return '';
}

sub run {
    my ($self, $context) = @_;
    local $_ = $context;
    $self->{code}->($context);
}

package FSM::Tiny::Guard;

sub key { shift->{key} }

sub code { shift->{code} }

sub new {
    my $package = shift;
    my %args = @_;
    bless +{ key  => '', code => sub { 1 }, %args }, $package;
}

sub check {
    my ($self, $context) = @_;
    return $self->code->($context);
}

1;
__END__

=head1 NAME

FSM::Tiny - tiny implementation of finite state machine

=head1 VERSION

This document describes FSM::Tiny version 0.01.

=head1 SYNOPSIS

    use FSM::Tiny;
    
    my $fsm = FSM::Tiny->new({
        on_enter => sub {
            $_->{count} = 0;
        }
    });
    
    $fsm->register(init => sub {}, [
        add => sub { $_->{count} < 20 },
        end => sub { $_->{count} >= 20 }
    ]);
    
    $fsm->register(add => sub { ++$_->{count} }, [
        init => 1
    ]);
    
    $fsm->register(end => sub { $_->{count} *= 5 });
    
    $fsm->run;

    print $fsm->context->{count}; # => 100


=head1 DESCRIPTION

This module is tiny implementation of finite state machine.
this provides more simpler interface and code than any cpan's FSM::* modules.

=head2 ATTRIBUTES

=head3 C<< current >>

define current state name for this machine.

=head3 C<< rules >>

same as register function.

=head3 C<< context >>

this is global variable of machine.
in state behavior(as function) and guard function, it is read as $_

=head3 C<< on_enter >>

it calls when machine transitions start.

=head3 C<< on_transition >>

it calls in between transitions.

=head3 C<< on_exit >>

it calls when machine transitions end.

=head2 METHODS

=head3 C<< new(%args) >>

you can define all rules and attributes in this initializer.

=head3 C<< register($state_name => $state_fn, [%conditions]) >>

registering state, state behavior(as function), and conditions for transition.
%conditions is defined follows:

    [
      destination1 => sub { !!it_should_move_to_destination1_or_not() },
      destination2 => sub { !!it_should_move_to_destination2_or_not() }
    ]

=head3 C<< step >>

it run one transition.

=head3 C<< run >>

it makes run until transitions end.

=head1 DEPENDENCIES

Perl 5.8.1 or later.

=head1 BUGS

All complex software has bugs lurking in it, and this module is no
exception. If you find a bug please either email me, or add the bug
to cpan-RT.

=head1 SEE ALSO

L<perl>

=head1 AUTHOR

<Taiyoh Tanaka> E<lt><sun.basix@gmail.com>E<gt>

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2013, <Taiyoh Tanaka>. All rights reserved.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut