package Function::Interface;
use v5.14.0;
use warnings;
our $VERSION = "0.06";
use Carp qw(croak);
use Keyword::Simple;
use PPR;
use Function::Interface::Info;
use Function::Interface::Info::Function;
use Function::Interface::Info::Function::Param;
use Function::Interface::Info::Function::ReturnParam;
sub import {
my $class = shift;
my %args = @_;
my $pkg = $args{pkg} ? $args{pkg} : caller;
Keyword::Simple::define 'fun' => _define_interface($pkg, 'fun');
Keyword::Simple::define 'method' => _define_interface($pkg, 'method');
}
sub unimport {
Keyword::Simple::undefine 'fun';
Keyword::Simple::undefine 'method';
}
sub _define_interface {
my ($pkg, $keyword) = @_;
return sub {
my $ref = shift;
my $match = _assert_valid_interface($$ref);
my $src = _render_src($pkg, $keyword, $match);
substr($$ref, 0, length $match->{statement}) = $src;
}
}
sub _render_src {
my ($pkg, $keyword, $match) = @_;
my $src = <<"```";
BEGIN {
Function::Interface::_register_info({
package => '$pkg',
keyword => '$keyword',
subname => '$match->{subname}',
params => [ @{[ join ',', map {
my $named = $_->{named} ? 1 : 0;
my $optional = $_->{optional} ? 1 : 0;
qq!{ type => $_->{type}, name => '$_->{name}', named => $named, optional => $optional }!
} @{$match->{params}} ]} ],
return => [ @{[ join ',', @{$match->{return}}] } ],
});
}
```
return $src;
}
our %metadata;
sub _register_info {
my ($args) = @_;
push @{$metadata{$args->{package}}} => +{
subname => $args->{subname},
keyword => $args->{keyword},
params => $args->{params},
return => $args->{return},
};
}
sub info {
my ($interface_package) = @_;
my $info = $metadata{$interface_package} or return undef;
Function::Interface::Info->new(
package => $interface_package,
functions => [ map {
Function::Interface::Info::Function->new(
subname => $_->{subname},
keyword => $_->{keyword},
params => [ map { _make_function_param($_) } @{$_->{params}} ],
return => [ map { _make_function_return_param($_) } @{$_->{return}} ],
)
} @{$info}],
);
}
sub _make_function_param {
my $param = shift;
Function::Interface::Info::Function::Param->new(
type => $param->{type},
name => $param->{name},
named => $param->{named},
optional => $param->{optional},
)
}
sub _make_function_return_param {
my $type = shift;
Function::Interface::Info::Function::ReturnParam->new(
type => $type,
)
}
sub _assert_valid_interface {
my $src = shift;
$src =~ m{
\A
(?<statement>
(?&PerlOWS) (?<subname>(?&PerlIdentifier))
(?&PerlOWS) \((?<params>.*?)\)
(?&PerlOWS) :Return\((?<return>.*?)\)
;
)
$PPR::GRAMMAR
}sx or croak "invalid interface";
my %match;
$match{statement} = $+{statement};
$match{subname} = $+{subname};
$match{params} = $+{params} ? _assert_valid_interface_params($+{params}) : [];
$match{return} = $+{return} ? _assert_valid_interface_return($+{return}) : [];
return \%match;
}
$Function::Interface::GRAMMAR = qr{
(?(DEFINE)
(?<PerlType>
(?&PerlIdentifier)
(?: \s* \[
\s* (?&PerlTypeParameter) \s*
(?: , \s* (?&PerlTypeParameter) \s* )*+
\] )?
)
(?<PerlTypeParameter>
(?&PerlString)|(?&PerlVariable)|(?&PerlType)
)
)
$PPR::GRAMMAR
}x;
sub _assert_valid_interface_params {
my $src = shift;
my @list = grep { defined } $src =~ m{
((?&PerlType)) \s*
(:?) # named \s*
((?&PerlVariable)) \s*
(=?) # optional
$Function::Interface::GRAMMAR
}xg;
my @params;
while (my ($type, $named, $name, $optional) = splice @list, 0, 4) {
push @params => {
type => $type,
named => !!$named,
name => $name,
optional => !!$optional,
}
}
my $regex = join '\s*,\s*', map {
quotemeta sprintf('%s %s%s%s',
$_->{type},
$_->{named} ? ':' : '',
$_->{name},
$_->{optional} ? '=' : '',
)
} @params;
croak "invalid interface params: $src"
unless $src =~ m{ \A \s* $regex \s* \z }x;
return \@params;
}
sub _assert_valid_interface_return {
my $src = shift;
my @list = grep { defined } $src =~ m{
((?&PerlType))
$Function::Interface::GRAMMAR
}xg;
croak "invalid interface return: $src. It should be TYPELIST."
unless $src =~ m{
\A \s* @{[join '\s*,\s*', map { quotemeta $_ } @list]} \s* \z
}x;
return \@list;
}
1;
__END__
=encoding utf-8
=head1 NAME
Function::Interface - declare typed interface package
=head1 SYNOPSIS
Declare typed interface package C<IFoo>:
package IFoo {
use Function::Interface;
use Types::Standard -types;
fun hello(Str $msg) :Return(Str);
fun add(Int $a, Int $b) :Return(Int);
}
Implements the interface package C<IFoo>:
package Foo {
use Function::Interface::Impl qw(IFoo);
use Types::Standard -types;
fun hello(Str $msg) :Return(Str) {
return "HELLO $msg";
}
fun add(Int $a, Int $b) :Return(Int) {
return $a + $b;
}
}
Use the type C<ImplOf>:
package FooService {
use Function::Interface::Types qw(ImplOf);
use Function::Parameters;
use Function::Return;
use Mouse;
use aliased 'IFoo';
fun greet(ImplOf[IFoo] $foo) :Return() {
print $foo->hello;
return;
}
}
my $foo_service = FooService->new;
my $foo = Foo->new; # implements of IFoo
$foo_service->greet($foo);
=head1 DESCRIPTION
This module provides a typed interface.
C<Function::Interface> declares abstract functions without implementation and defines an interface package.
C<Function::Interface::Impl> checks if the abstract functions are implemented at B<compile time>.
=head2 SUPPORT
This module supports all perl versions starting from v5.14.
=head2 Declare function
C<Function::Interface> provides two new keywords, C<fun> and C<method>, for declaring abstract functions and methods with types:
fun hello(Str $msg) :Return(Str);
method new(Num :$x, Num :$y) :Return(Point);
The method of declaring abstract functions is the same as L<Function::Parameters> and L<Function::Return>.
=head3 declare parameters
Function arguments must always specify a variable name and type constraint, and named arguments and optional arguments can optionally be specified:
# positional parameters
# e.g. called `foo(1,2,3)`
fun foo1(Int $a, Int $b, Int $c) :Return();
# named parameters
# e.g. called `bar(x => 123, y => 456)`
fun foo2(Num :$x, Num :$y) :Return();
# optional
# e.g. called `baz()` or `baz('some')`
fun foo3(Str $msg=) :Return();
=head3 declare return types
Specify zero or more type constraints for the function's return value:
# zero(empty)
fun bar1() :Return();
# one
fun bar2() :Return(Str);
# two
fun bar3() :Return(Str, Num);
=head2 requirements of type constraint
The requirements of type constraint of C<Function::Interface> is the same as for L<Function::Parameters> and L<Function::Return>.
=head1 METHODS
=head2 Function::Interface::info($interface_package)
The function C<Function::Interface::info> lets you introspect interface functions:
# declare interface package
package IBar {
use Function::Interface;
fun hello() :Return();
fun world() :Return();
}
# introspect
my $info = Function::Interface::info 'IBar';
$info->package; # => IBar
$info->functions; # => list of Function::Interface::Info::Function
It returns either C<undef> if it knows nothing about the interface or an object of L<Function::Interface::Info>.
=head1 SEE ALSO
L<Function::Interface::Impl>
=head1 LICENSE
Copyright (C) kfly8.
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=head1 AUTHOR
kfly8 E<lt>[email protected]<gt>
=cut