use strict;
use warnings;
package Boilerplater::Binding::Perl::Class;
use Boilerplater::Util qw( verify_args );
our %registry;
sub registry { \%registry }
our %register_PARAMS = (
parcel => undef,
class_name => undef,
bind_methods => undef,
bind_constructors => undef,
make_pod => undef,
xs_code => undef,
client => undef,
);
sub register {
my $either = shift;
verify_args( \%register_PARAMS, @_ ) or confess $@;
my $self = bless { %register_PARAMS, @_, }, ref($either) || $either;
confess("Missing required param 'class_name'")
unless $self->{class_name};
confess("$self->{class_name} already registered")
if exists $registry{ $self->{class_name} };
if ( $self->{bind_methods}
|| $self->{bind_constructors}
|| $self->{make_pod} )
{
$self->{client} = Boilerplater::Class->fetch_singleton(
parcel => $self->{parcel},
class_name => $self->{class_name},
);
confess("Can't fetch singleton for $self->{class_name}")
unless $self->{client};
}
$registry{ $self->{class_name} } = $self;
return $self;
}
sub get_class_name { shift->{class_name} }
sub get_bind_methods { shift->{bind_methods} }
sub get_bind_constructors { shift->{bind_constructors} }
sub get_make_pod { shift->{make_pod} }
sub get_client { shift->{client} }
sub get_xs_code { shift->{xs_code} }
sub constructor_bindings {
my $self = shift;
my @bound = map {
my $xsub = Boilerplater::Binding::Perl::Constructor->new(
class => $self->{client},
alias => $_,
);
} @{ $self->{bind_constructors} };
return @bound;
}
sub method_bindings {
my $self = shift;
my $client = $self->{client};
my $meth_list = $self->{bind_methods};
my @bound;
# Assemble a list of methods to be bound for this class.
my %meth_to_bind;
for my $meth_namespec (@$meth_list) {
my ( $alias, $name )
= $meth_namespec =~ /^(.*?)\|(.*)$/
? ( $1, $2 )
: ( lc($meth_namespec), $meth_namespec );
$meth_to_bind{$name} = { alias => $alias };
}
# Iterate over all this class's methods, stopping to bind each one that
# was spec'd.
for my $method ( $client->methods ) {
my $meth_name = $method->get_macro_sym;
my $bind_args = delete $meth_to_bind{$meth_name};
next unless $bind_args;
# Safety checks against excess binding code or private methods.
if ( !$method->novel ) {
confess( "Binding spec'd for method '$meth_name' in class "
. "$self->{class_name}, but it's overridden and "
. "should be bound via the parent class" );
}
elsif ( $method->private ) {
confess( "Binding spec'd for method '$meth_name' in class "
. "$self->{class_name}, but it's private" );
}
# Create an XSub binding for each override. Each of these directly
# calls the implementing function, rather than invokes the method on
# the object using VTable method dispatch. Doing things this way
# allows SUPER:: invocations from Perl-space to work properly.
for my $descendant ( $client->tree_to_ladder ) { # includes self
my $real_method = $descendant->novel_method( lc($meth_name) );
next unless $real_method;
# Create the binding, add it to the array.
my $method_binding = Boilerplater::Binding::Perl::Method->new(
method => $real_method,
%$bind_args,
);
push @bound, $method_binding;
}
}
# Verify that we processed all methods.
my @leftover_meths = keys %meth_to_bind;
confess("Leftover for $self->{class_name}: '@leftover_meths'")
if @leftover_meths;
return @bound;
}
sub _gen_subroutine_pod {
my ( $self, %args ) = @_;
my ( $func, $sub_name, $class, $code_sample, $class_name )
= @args{qw( func name class sample class_name )};
my $param_list = $func->get_param_list;
my $args = "";
my $num_vars = $param_list->num_vars;
# Only allow "public" subs to be exposed as part of the public API.
confess("$class_name->$sub_name is not public") unless $func->public;
# Get documentation, which may be inherited.
my $docucom = $func->get_docucomment;
if ( !$docucom ) {
my $micro_sym = $func->micro_sym;
my $parent = $class;
while ( $parent = $parent->get_parent ) {
my $parent_func = $parent->method($micro_sym);
last unless $parent_func;
$docucom = $parent_func->get_docucomment;
last if $docucom;
}
}
confess("No DocuComment for '$sub_name' in '$class_name'")
unless $docucom;
if ( $num_vars > 2 or ( $args{is_constructor} && $num_vars > 1 ) ) {
$args = " I<[labeled params]> ";
}
elsif ( $param_list->num_vars ) {
$args = $func->get_param_list->name_list;
$args =~ s/self.*?(?:,\s*|$)//; # kill self param
}
my $pod = "=head2 $sub_name($args)\n\n";
if ( defined($code_sample) && length($code_sample) ) {
$pod .= "$code_sample\n";
}
if ( my $long_doc = $docucom->get_description ) {
$pod .= _perlify_doc_text($long_doc) . "\n\n";
}
# Add params in a list.
my $param_names = $docucom->get_param_names;
my $param_docs = $docucom->get_param_docs;
if (@$param_names) {
$pod .= "=over\n\n";
for ( my $i = 0; $i <= $#$param_names; $i++ ) {
$pod .= "=item *\n\n";
$pod .= "B<$param_names->[$i]> - $param_docs->[$i]\n\n";
}
$pod .= "=back\n\n";
}
# Add return value description, if any.
if ( defined( my $retval = $docucom->get_retval ) ) {
$pod .= "Returns: $retval\n\n";
}
return $pod;
}
sub create_pod {
my $self = shift;
my $pod_args = $self->{make_pod} or return;
my $class = $self->{client} or die "No client for $self->{class_name}";
my $class_name = $class->get_class_name;
my $docucom = $class->get_docucomment;
confess("No DocuComment for '$class_name'") unless $docucom;
my $brief = $docucom->get_brief;
my $description = _perlify_doc_text( $pod_args->{description}
|| $docucom->get_description );
my $synopsis_pod = '';
if ( defined $pod_args->{synopsis} ) {
$synopsis_pod = qq|=head1 SYNOPSIS\n\n$pod_args->{synopsis}\n|;
}
my $constructor_pod = "";
my $constructors = $pod_args->{constructors} || [];
if ( defined $pod_args->{constructor} ) {
push @$constructors, $pod_args->{constructor};
}
if (@$constructors) {
$constructor_pod = "=head1 CONSTRUCTORS\n\n";
for my $spec (@$constructors) {
if ( !ref $spec ) {
$constructor_pod .= _perlify_doc_text($spec);
}
else {
my $func_name = $spec->{func} || 'init';
my $init_func = $class->function($func_name);
my $ctor_name = $spec->{name} || 'new';
my $code_sample = $spec->{sample};
$constructor_pod .= _perlify_doc_text(
$self->_gen_subroutine_pod(
func => $init_func,
name => $ctor_name,
sample => $code_sample,
class => $class,
class_name => $class_name,
is_constructor => 1,
)
);
}
}
}
my @method_docs;
my $methods_pod = "";
my @abstract_method_docs;
my $abstract_methods_pod = "";
for my $spec ( @{ $pod_args->{methods} } ) {
my $meth_name = ref($spec) ? $spec->{name} : $spec;
my $method = $class->method($meth_name);
confess("Can't find method '$meth_name' in class '$class_name'")
unless $method;
my $method_pod;
if ( ref($spec) ) {
$method_pod = $spec->{pod};
}
else {
$method_pod = $self->_gen_subroutine_pod(
func => $method,
name => $meth_name,
sample => '',
class => $class,
class_name => $class_name
);
}
if ( $method->abstract ) {
push @abstract_method_docs, _perlify_doc_text($method_pod);
}
else {
push @method_docs, _perlify_doc_text($method_pod);
}
}
if (@method_docs) {
$methods_pod = join( "", "=head1 METHODS\n\n", @method_docs );
}
if (@abstract_method_docs) {
$abstract_methods_pod = join( "", "=head1 ABSTRACT METHODS\n\n",
@abstract_method_docs );
}
my $child = $class;
my @ancestors;
while ( defined( my $parent = $child->get_parent ) ) {
push @ancestors, $parent;
$child = $parent;
}
my $inheritance_pod = "";
if (@ancestors) {
$inheritance_pod = "=head1 INHERITANCE\n\n";
$inheritance_pod .= $class->get_class_name;
for my $ancestor (@ancestors) {
$inheritance_pod .= " isa L<" . $ancestor->get_class_name . ">";
}
$inheritance_pod .= ".\n";
}
my $pod = <<END_POD;
# Auto-generated file -- DO NOT EDIT!!!!!
=head1 NAME
$class_name - $brief
$synopsis_pod
=head1 DESCRIPTION
$description
$constructor_pod
$methods_pod
$abstract_methods_pod
$inheritance_pod
=head1 COPYRIGHT AND LICENSE
Copyright 2005-2009 Marvin Humphrey
This program is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.
=cut
END_POD
}
sub _perlify_doc_text {
my $documentation = shift;
# Remove double-equals hack needed to fool perldoc, PAUSE, etc. :P
$documentation =~ s/^==/=/mg;
# Change <code>foo</code> to C<< foo >>.
$documentation =~ s#<code>(.*?)</code>#C<< $1 >>#gsm;
# Lowercase all method names: Open_In() => open_in()
$documentation
=~ s/([A-Z][A-Za-z0-9]*(?:_[A-Z][A-Za-z0-9]*)*\(\))/\L$1\E/gsm;
# Change all instances of NULL to 'undef'
$documentation =~ s/NULL/undef/g;
return $documentation;
}
1;
__END__
__POD__
=head1 NAME
Boilerplater::Binding::Perl::Class - Generate Perl binding code for a
Boilerplater::Class.
=head1 COPYRIGHT AND LICENSE
Copyright 2008-2009 Marvin Humphrey
This program is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.
=cut