Evented-API-Engine

 view release on metacpan or  search on metacpan

lib/Evented/API/Engine.pm  view on Meta::CPAN

# Copyright (c) 2017, Mitchell Cooper
#
# the Evented API Engine is in charge of loading and unloading modules.
# it also handles dependencies and feature availability.
#
package Evented::API::Engine;

use warnings;
use strict;
use 5.010;

use JSON::XS;
use Scalar::Util qw(weaken blessed);
use Module::Loaded qw(mark_as_loaded mark_as_unloaded is_loaded);
use Evented::Object;
use parent 'Evented::Object';

our $VERSION = '4.13';

use Evented::API::Module;
use Evented::API::Events;
use Evented::Object::Hax qw(set_symbol make_child);

=head1 NAME

B<Evented::API::Engine> - an Evented API Engine for Perl applications.

=head1 SYNOPSIS

Main

    my $api = Evented::API::Engine->new;
    $api->load_module('My::Module');

My::Module

    # Module metadata
    #
    # @name:        'My::Module'
    # @package:     'M::My::Module'
    # @description:
    #
    # @depends.modules+ 'Some::Other'
    # @depends.modules+ 'Another::Yet'
    #
    # @author.name:     'Mitchell Cooper'
    # @author.website:  'https://github.com/cooper'
    #
    package M::My::Module;
    
    use warnings;
    use strict;
    use 5.010;
    
    # Auto-exported variables
    our ($api, $mod);
    
    # Default initializer
    sub init {
        say 'Loading ', $mod->name;
        
        # indicates load success
        return 1;
    }
    
    # Default deinitializer
    sub void {
        say 'Bye!';
        
        # indicates unload success
        return 1;
    }
    

lib/Evented/API/Engine.pm  view on Meta::CPAN

    # find packages
    if (!$info->{package}) {
        $api->Log($mod_name, "Load FAILED: \@package is required");
        return;
    }
    
    $info->{package} = [ $info->{package} ]
        if ref $info->{package} ne 'ARRAY';
    my @pkgs = @{ $info->{package} };
    my $pkg = $pkgs[0] or return; # main pkg (module constructor)


    # LOAD DEPENDENCIES
    #------------------------

    # load required module dependencies here.
    # consider: if the module fails to load, the dependencies remain loaded.
    $api->_load_module_requirements($info) or return;


    # CREATE MODULE OBJECT
    #------------------------

    # make the module package a child of Evented::API::Module
    # unless 'no_bless' is true.
    my $constructor = 'Evented::API::Module';
    unless ($info->{no_bless}) {
        make_child($pkg, $constructor);
        $pkg->isa($constructor) or return;
        $constructor = $pkg;
    }

    # create the module object.
    my $mod = $constructor->new(
        %$info,
        dir => $mod_dir,
        reloading => $reloading
    );

    # the constructor returned bogus or nothing.
    if (!$mod || !$mod->isa('Evented::API::Module')) {
        $api->Log($mod_name,
            "Load FAILED: Constructor $constructor->new() failed");
        _package_unload(@pkgs);
        return;
    }

    # Safe point - the module object is available and safe for use.
    # add it to the list of loaded modules.
    push @{ $api->{loaded} }, $mod;

    # store dependecy module objects.
    $mod->{dependencies} = [
        map { $api->get_module($_) }    # this definitely is an arrayref;
        @{ $info->{depends}{modules} }  # verified @ ->_load_module_requirements
    ];

    # make the API Engine listen to the events of the module.
    # hold a weak reference to the API engine.
    $mod->add_listener($api, 'module');
    weaken($mod->{api} = $api);

    # here we fire an event which will export symbols for convenient use
    # within the module packages. see Module.pm for defaults.
    $mod->fire(set_variables => $_) for @pkgs;


    # EVALUATE
    #------------------------

    my $return;
    $mod->Debug("Evaluating $mod_last_name.pm");
    {

        # disable warnings on redefinition.
        no warnings 'redefine';

        # capture other warnings.
        local $SIG{__WARN__} = sub {
            my $warn = shift;
            chomp $warn;
            $mod->Log("WARNING: $warn");
        };

        # do() the file.
        $return = do "$mod_dir/$mod_last_name.pm";

    }

    # an error occurred in loading.
    if (!$return || $return != $mod) {
        my $error = $@ || $! || 'Package did not return module object';
        $mod->Log('Load FAILED: '.$error);
        $api->_abort_module_load($mod);
        return;
    }


    # INITIALIZE
    #------------------------

    # initialize the module. returns false on fail.
    $mod->_do_init or return;

    # Safe point - the module will certainly remain loaded.


    # POST-LOAD
    #------------------------

    # fire the 'load' event to indicate it has finished loading.
    $mod->fire('load');
    $mod->Log("Loaded successfully ($$mod{version})");

    # mark the packages as loaded.
    for my $package (@pkgs) {
        mark_as_loaded($package) unless is_loaded($package);
    }

    # load postponed companion submodules, if any.
    $api->_load_companion_submodules($mod);

lib/Evented/API/Engine.pm  view on Meta::CPAN

        # not blessed, search for module.
        if (!blessed $mod) {
            $mod = $api->get_module($mod);
            if (!$mod) {
                $api->Log($_[1], 'Reload: not loaded');
                next;
            }
        }

        # unload the module.
        $mod->{reloading} = 1;
        # ($mod, $unload_dependents, $unload_parent, $unloading_submodule, $reloading)
        $api->unload_module($mod, 1, 1, undef, 1) or return;
    }

    # load all of the modules that were unloaded again
    # (if they weren't already loaded, probably as dependencies).
    my $unloaded = delete $api->{r_unloaded};
    while (my $mod_name = shift @$unloaded) {
        next if $api->module_loaded($mod_name);
        # ($mod_name, $dirs, $is_submodule, $reloading);
        $count++ if $api->load_module($mod_name, undef, undef, 1);
    }

    return $count;
}

=head2 $api->reload_modules(@mods)

Reloads one or more modules at once. See C<< ->reload_module() >>.

B<Parameters>

=over

=item *

B<@mods> - module objects or names to reload.

=back

B<Returns>

Number of modules reloaded successfully, false if all failed.

=cut

sub reload_modules;
*reload_modules = *reload_module;

############################
### COMPANION SUBMODULES ###
############################

sub _add_companion_submodule_wait {
    my ($api, $mod, $mod_name, $submod_name) = @_;

    # postpone load until the companion is loaded.
    # hold a weak reference to the module waiting.
    my $waits = $api->{companion_waits}{$mod_name} ||= [];
    my $ref = [ $mod, $submod_name ]; weaken($ref->[0]);
    push @$waits, $ref;

    # if it is already loaded, go ahead and load the submodule.
    if (my $loaded = $api->get_module($mod_name)) {
        return $api->_load_companion_submodules($loaded);
    }

    # false return indicates not yet loaded.
    return;
}

sub _load_companion_submodules {
    my ($api, $mod) = @_;
    my $waits = delete $api->{companion_waits}{ $mod->name } or return;
    ref $waits eq 'ARRAY' or return;

    my $status;
    foreach (@$waits) {
        my ($parent_mod, $submod_name) = @$_;

        # load the submodule.
        $parent_mod->Log("Loading companion submodule");
        my $submod = $parent_mod->load_submodule($submod_name) or next;

        # remember that this submodule depends on $mod.
        push @{ $submod->{companions} ||= [] }, $mod;

    }

    return $status;
}

########################
### FETCHING MODULES ###
########################

=head2 $api->get_module($mod_name)

Fetches a loaded module object.

B<Parameters>

=over

=item *

B<$mod_name> - name of the module to find.

=back

B<Returns>

L<Module object on success|Evented::API::Module>, false otherwise.

=cut

# returns the module object of a full module name.
sub get_module {
    my ($api, $mod_name) = @_;
    foreach (@{ $api->{loaded} }) {



( run in 2.952 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )