The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Perinci::Use;

our $DATE = '2017-07-10'; # DATE
our $VERSION = '0.07'; # VERSION

use 5.010001;
use strict;
use warnings;
use experimental 'smartmatch';
use Log::ger;

use Perinci::Access;
use Perinci::Sub::Util qw(err);

our %SPEC;

$SPEC{use_riap_package} = {
    v => 1.1,
    summary => 'Use a Riap package as if it was a local Perl module',
    result => {
        schema => 'undef',
    },
    description => <<'_',

"Use" a remote code package over Riap protocol as if it was a local Perl module.
Actually, what is being done is query the remote URL for available functions,
and for each remote function, create a proxy function. The proxy function will
then call the remote function.

Currently only functions are imported. Variables and other entities are ignored.

_
    args => {
        url => {
            summary => 'Location of Riap package entity',
            description => <<'_',

Example: '/Foo/Bar/' (local Perl module), 'http://example.com/api/Module/'.

_
            schema => 'str*',
            req => 1,
            pos => 0,
        },
        into => {
            summary     => 'Perl package name to put proxy functions into',
            description => <<'_',

Example: 'Foo::Bar', 'Example::Module'.

_
            schema => 'str*',
            req => 1,
            pos => 1,
        },
        include => {
            summary     => 'Do not load all children, only load specified ones',
            schema => ['array*' => of=>'str*'],
        },
    },
};
sub use_riap_package {
    my %args   = @_;
    log_trace("-> use_riap_package(%s)", \%args);
    my $url    = $args{url}  or return [400, "Please specify url"];
    my $into   = $args{into} or return [400, "Please specify into"];
    return [500, "Invalid module name `$into`"]
        unless $into =~ /\A\w+(::\w+)*\z/;
    my $inc    = $args{include} // [];

    my $pa = Perinci::Access->new;

    # try child_metas first
    my $res = $pa->request(child_metas => $url);
    return err(500, "Can't request action 'child_metas' on URL $url", $res)
        unless $res->[0] == 200 || $res->[0] == 501;

    my @e;
    if ($res->[0] == 200) {
        my $metas = $res->[2];
        for my $u (keys %$metas) {
            my $meta = $metas->{$u};
            next unless $meta->{args};
            my $sub = $u; $sub =~ s!.+/!!;
            push @e, [$sub, $u, $meta];
        }
    } else {
        # try 'list' + later 'meta' for each child
        $res = $pa->request(list => $url, {detail=>1});
        return err(500, "Can't request action 'list' on URL $url", $res)
            unless $res->[0] == 200;
        for my $r (@{$res->[2]}) {
            next unless $r->{type} eq 'function';
            my $sub = $r->{uri}; $sub =~ s!.+/!!;
            push @e, [$sub, $r->{uri}];
        }
    }

    # check all specified entries 'include' must exist
    for my $s (@$inc) {
        return [400, "'$s' does not exist under $url"]
            unless grep {$s eq $_->[0]} @e;
    }

    # create proxy functions
    for my $e (@e) {
        next if @$inc && !($e->[0] ~~ @$inc);

        # get metadata if not yet retrieved
        unless ($e->[2]) {
            $res = $pa->request(meta => $e->[1]);
            return err(500, "Can't request action 'meta' on URL $e->[1]", $res)
                unless $res->[0] == 200;
            $e->[2] = $res->[2];
        }

        # mark metadata
        $e->[2]{_note} = "Imported by ".__PACKAGE__." on ".scalar(localtime);

        # create proxy
        no strict 'refs';
        no warnings;
        *{"$into\::$e->[0]"} = sub {
            my %args = @_;
            $pa->request(call => $e->[1], {args=>\%args});
        };
        ${"$into\::SPEC"}{$e->[0]} = $e->[2];
    }

    log_trace("<- use_riap_package()");
    [200, "OK"];
}

sub import {
    my ($module, $url, @args) = @_;
    my $into = caller;

    die "import: Please specify URL as first argument" unless $url;

    my $into_pm = $into; $into_pm =~ s!::!/!g; $into_pm .= ".pm";
    return if $INC{$into_pm};

    my $res = use_riap_package(url=>$url, into=>$into, include=>\@args);
    die "import: Can't use_riap_package $url: $res->[0] - $res->[1]"
        unless $res->[0] == 200;

    $INC{$into_pm} = $url;

    1;
}

1;
# ABSTRACT: Use a Riap package as if it was a local Perl module

__END__

=pod

=encoding UTF-8

=head1 NAME

Perinci::Use - Use a Riap package as if it was a local Perl module

=head1 VERSION

This document describes version 0.07 of Perinci::Use (from Perl distribution Perinci-Use), released on 2017-07-10.

=head1 SYNOPSIS

 # import pyth()
 use Perinci::Use "http://example.com/My/Math", 'pyth';
 print pyth(3, 4); # 5

 # import all
 use Perinci::Use "http://example.com/My/Math";

=head1 DESCRIPTION

This module provides use_riap_package(), usually used as shown in Synopsis, a la
Perl's use().

=head1 FUNCTIONS


=head2 use_riap_package

Usage:

 use_riap_package(%args) -> [status, msg, result, meta]

Use a Riap package as if it was a local Perl module.

"Use" a remote code package over Riap protocol as if it was a local Perl module.
Actually, what is being done is query the remote URL for available functions,
and for each remote function, create a proxy function. The proxy function will
then call the remote function.

Currently only functions are imported. Variables and other entities are ignored.

This function is not exported.

Arguments ('*' denotes required arguments):

=over 4

=item * B<include> => I<array[str]>

Do not load all children, only load specified ones.

=item * B<into>* => I<str>

Perl package name to put proxy functions into.

Example: 'Foo::Bar', 'Example::Module'.

=item * B<url>* => I<str>

Location of Riap package entity.

Example: '/Foo/Bar/' (local Perl module), 'http://example.com/api/Module/'.

=back

Returns an enveloped result (an array).

First element (status) is an integer containing HTTP status code
(200 means OK, 4xx caller error, 5xx function error). Second element
(msg) is a string containing error message, or 'OK' if status is
200. Third element (result) is optional, the actual result. Fourth
element (meta) is called result metadata and is optional, a hash
that contains extra information.

Return value:  (undef)

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Use>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-Perinci-Use>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Use>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 SEE ALSO

L<Perinci::Access>

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017, 2015, 2014, 2012 by perlancar@cpan.org.

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

=cut