NAME

portable::loader - load classes and roles which can be moved around your namespace

SYNOPSIS

Define some classes:

## Nature.portable
##
version = 1.0
toolkit = "Moo"

[class:Tree.has]
leaf = { is = "lazy", type = "ArrayRef[Leaf]" }

[class:Tree.can]
add_leaf = {{{
  my $self = shift;
  push @{ $self->leaf }, @_;
  return $self; # for chaining
}}}
_build_leaf = {{{
  return [];
}}}

[class:Leaf.has]
colour = { type = "Str", default = "green" }

[class:Maple]
extends = "Tree"

Use the classes:

## script.pl
##
use portable::lib '/var/lib/portable-libs';
use portable::alias 'Nature';

my $tree  = Nature->new_maple;
$tree->add_leaf( Nature->new_leaf );

# 'Nature' isn't really a Perl package.
# It's just a sub that returns a string.

DESCRIPTION

The intent of portable::loader is for classes and roles to be portable around your namespace. The idea is for classes and roles to not know their package names and not care about their package names. And for them to also not know or care about the package names of their "friends".

(When I say their friends, I'm talking about a user-agent object which needs to be able to consume HTTP request objects and return HTTP response objects, maybe write to a cookie jar object, etc.)

Typically in Perl code, package names are the one thing that is hard-coded everywhere and this can make things like dependency injection, and API versioning really difficult to do. Like if you need to make some major changes to your class's API, do you create an entirely new package with a different namespace, then wait for your consumers to update? Or do you keep the old namespace and deal with breakages.

What if instead of doing this:

use YourAPI::Tree;
use YourAPI::Leaf;

my $tree = YourAPI::Tree->new;
$tree->add_leaf(YourAPI::Leaf->new);

People could do this?

use portable::loader;
my $api = portable::loader->load("YourAPI");

my $tree = $api->new_tree;
$tree->add_leaf($api->new_leaf);

The class names are not hard-coded anywhere. They are not even hard-coded in the definitions of the Leaf and Tree classes.

And there's very little runtime overhead in doing this!

Writing a portable library

Syntax

Portable libraries are conceptually any hashref suitable for passing to MooX::Press. A structure something like this:

{
  version => 1.0,
  toolkit => "Moo",
  "class:Tree" => {
    has => [
      "leaf" => { is => "lazy", type => "ArrayRef[Leaf]" },
    ],
    can => [
      "add_leaf" => sub {
        my $self = shift;
        push @{ $self->leaf }, @_;
        return $self; # for chaining
      },
      "_build_leaf" => sub {
        return [];
      },
    ],
  },
  "class:Leaf" => {
    has => [
      "colour" => { type => "Str", default => "green" },
    ],
  },
  "class:Maple" => {
    extends => "Tree",
  },
}

You could save that as "Nature.portable.pl" and portable::loader would be able to load it.

But although a library is conceptually a hashref, it can be written in other syntaxes. It could be written in JSON, if JSON::Eval is used to inflate coderefs in the JSON:

{
  "version": 1.0,
  "toolkit": "Moo",
  "class:Tree": {
    "has": [
      "leaf": { "is": "lazy", "type": "ArrayRef[Leaf]" }
    ],
    "can": [
      "add_leaf": {
        "$eval": "sub { my $self = shift; push @{ $self->leaf }, @_; return $self; }" 
      },
      "_build_leaf: {
        "$eval": "sub { return []; }"
      }
    ]
  },
  "class:Leaf": {
    "has": [
      "colour": { "type": "Str", "default": "green" }
    ],
  },
  "class:Maple": {
    "extends": "Tree"
  }
}

If this is saved at "Nature.portable.json", portable::loader should be able to load it.

The default format that portable::loader uses though, is TOML, an INI-like file format. portable::loader adds an extension to TOML allowing {{{ ... }}} to represent a coderef with Perl code inside. (The parsing is kind of naive, so don't expect nested coderefs to work and that kind of thing!

version = 1.0
toolkit = "Moo"

[class:Tree.has]
leaf = { is = "lazy", type = "ArrayRef[Leaf]" }

[class:Tree.can]
add_leaf = {{{
  my $self = shift;
  push @{ $self->leaf }, @_;
  return $self; # for chaining
}}}
_build_leaf = {{{
  return [];
}}}

[class:Leaf.has]
colour = { type = "Str", default = "green" }

[class:Maple]
extends = "Tree"

Design considerations

When writing a library, the key thing to remember is that you don't know the final package names of any of your classes and roles.

You can refer to other classes and roles from your library in type constraints, and that should "just work".

Also, you can instantiate other classes in your methods using:

[class:Maple.can]
grow_red_leaf = {{{
  my $self = shift;
  my $leaf = $self->FACTORY->new_leaf(colour => "red");
  push @{ $self->leaf }, $leaf;
  return $self;
}}}

The $self->FACTORY method gives you something with a bunch of new_* methods for instantiating other objects from your library.

You could even do this when defining the Leaf class:

[class:Leaf.factory]
new_leaf = {{{
  my ($factory, $class) = (shift, shift);
  return $class->new(@_);
}}}
new_red_leaf = {{{
  my ($factory, $class) = (shift, shift);
  return $class->new(colour => "red", @_);
}}}

And then your Maple class can do this:

[class:Maple.can]
grow_red_leaf = {{{
  my $self = shift;
  my $leaf = $self->FACTORY->new_red_leaf;
  push @{ $self->leaf }, $leaf;
  return $self;
}}}

The aim being for your Maple class to know as little as possible about how to build a leaf other than "I can get one from the factory".

This makes it easy to override behaviour using Class::Method::Modifiers to wrap the new_red_leaf method of the factory.

Loading a library

portable::loader maintains its own version of @INC to locate libraries from: @portable::INC.

You can use portable::lib to push directories onto it:

use portable::lib '/var/lib/portable-libs';

Or you can manipulate @portable::INC directly; it's just an array of strings. You should use portable::lib first though because portable::lib will push some default directories onto @portable::INC before it loads.

Once you've set your search paths, you can load a library like this:

use portable::loader;
my $lib = portable::loader->load($libname);

portable::loader will search for "$libname.portable.pl", "$libname.portable.json", "$libname.portable.toml", or "$libname.portable" (which will be assumed to be TOML). Other formats can be supported through plugins. (API will eventually be documented.)

It will be parsed, loaded, classes built, etc, and a string will be returned which can be used

If more than one is found, only one will be loaded. The order in which they are checked is currently not guaranteed, but the precedence of directories in @portable::INC will be respected.

There are also load_from_filename and load_from_hashref methods if you already know the exact filename you want to load, or already have a hashref.

Using portable::alias

This:

use portable::alias "Foo";

Is roughly equivalent to this:

use portable::loader;
use constant "Foo" => portable::loader->load("Foo");

This:

use portable::alias "VeryLongName" => "ShortName";

Means this:

use constant "ShortName" => portable::loader->load("VeryLongName");

So you can do:

my $thing = Foo->new_someclass(%args);

The "constant" exported by portable::alias isn't really a constant though. It accepts arguments. You can do:

my $thing = Foo("SomeClass")->new(%args);

my $type_constraint = Foo("SomeClass");
my $type_constraint = Foo("SomeRole");

Using portable::alias is a cleaner-looking alternative to using portable::loader in a lot of cases.

Using a library

Use the factory returned by portable::loader to create objects, then use the objects according to the library's documentation.

BUGS

Please report any bugs to http://rt.cpan.org/Dist/Display.html?Queue=portable-loader.

SEE ALSO

MooX::Press, JSON::Eval, TOML, Type::Tiny, Moo, Moose.

AUTHOR

Toby Inkster <tobyink@cpan.org>.

COPYRIGHT AND LICENCE

This software is copyright (c) 2019 by Toby Inkster.

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

DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.