package Package::FromData; use strict; use warnings; use 5.010; use base 'Exporter'; our @EXPORT = qw/create_package_from_data/; our $VERSION = '0.01'; use Readonly; use Carp; use Scalar::Util qw(blessed); use Test::Deep::NoTest qw(eq_deeply); Readonly my %SIGIL_TYPE_MAP => ( '$' => 'SCALAR', '@' => 'ARRAY', '%' => 'HASH', '*' => 'GLOB', ); sub _must_be($$$) { croak $_[0] unless ref $_[1] && ref $_[1] eq $_[2]; } sub _must_be_hash($$) { &_must_be(@_[0,1], 'HASH' ) } sub _must_be_array($$) { &_must_be(@_[0,1], 'ARRAY') } sub create_package_from_data { my $packages = shift; _must_be_hash 'please pass create_package_from_data a hashref', $packages; _must_be_hash 'definition for package must be a hashref', $_ for values %$packages; foreach my $package (keys %$packages){ my $def = $packages->{$package}; # create package _create_package($package); # add constructors foreach my $const (@{$def->{constructors}||[]}){ _add_constructor($package, $const); } # add variables my $sigils = '['. (join '', keys %SIGIL_TYPE_MAP). ']'; foreach my $variable (keys %{$def->{variables}||{}}){ if($variable !~ /^(?$sigils)(?\w+)$/o){ die "'$variable' doesn't look like a variable name"; } my $sigil = $+{sigil}; # XXX infer from reftype? my $varname = $+{varname}; my $value = $def->{variables}{$variable}; $value = \"$value" if !ref $value; # make scalar a SCALAR _must_be "value for '$variable' must be a ". $SIGIL_TYPE_MAP{$sigil}. ' reference', $value, $SIGIL_TYPE_MAP{$sigil}; _add_variable_to($package, $varname, $value); } # add functions foreach my $function (keys %{$def->{functions}||{}}){ _add_function_from_definition($package, $function, $def->{functions}{$function}); } # add methods foreach my $method (keys %{$def->{methods}||{}}){ _add_function_from_definition( $package, $method, $def->{methods}{$method}, 1, sub { croak 'must be called as a method' unless blessed $_[0] }, ); } # add static methods foreach my $method (keys %{$def->{static_methods}||{}}){ _add_function_from_definition( $package, $method, $def->{static_methods}{$method}, 1 ); } } } sub _create_package { my $name = shift; die "invalid package name '$name'" unless $name =~ /^\w(?:\w|::)+\w$/; eval "package $name"; } sub _add_constructor { my ($package, $name) = @_; _add_function_to($package, $name, sub { my $class = shift; return bless {}, $class }); } sub _mk_sub { my ($body, $shift, $precondition) = @_; return sub { $precondition->(@_) if $precondition; do { shift for (1..$shift) } if $shift; # kill unnecessary args return $body->(@_) if $body; } } sub _add_function_from_definition { my ($package, $function, $fdef, $shift, $precondition) = @_; given(ref $fdef){ when('ARRAY'){ my @fdef = @$fdef; my $func; # determine default my $default; $default = pop @fdef if @fdef % 2 == 1; # def is of the form { method => 'Class' } if(!@fdef && ref $default eq 'HASH' && scalar keys %$default == 1){ my ($method, $class) = %$default; $func = _mk_sub( sub { return $class->$method; }, $shift, $precondition); } # def is a [ [expected @_] => output, ... ] seq else { my @rules; for(my $i = 0; $i < @fdef; $i+=2){ my ($in, $out) = @fdef[$i,$i+1]; push @rules, _mk_matcher($in, $out); } $func = _mk_sub( sub { for(@rules){ my @result = $_->(wantarray, @_); if(@result){ return @result if(wantarray); return $result[0]; } } if (ref $default eq 'ARRAY'){ if(wantarray){ return @$default; } return $default->[0]; } return $default || die "$function cannot handle [@_] as input"; }, $shift, $precondition); } _add_function_to($package, $function, $func); } default { # constant function _add_constant_function_to($package, $function, $fdef); } } } sub _mk_matcher { my ($in, $out) = @_; my @in = @$in; my @out = ($out); @out = @$out if ref $out eq 'ARRAY'; return sub { my $wantarray = shift; if (eq_deeply [@_], [@in]){ if(ref $out eq 'HASH'){ if($wantarray){ return @{$out->{list}||$out->{array}}; } return $out->{scalar}; } return @out; } return; } } sub _add_constant_function_to { my ($package, $function, $value) = @_; _add_function_to($package, $function, sub { $value }); } sub _add_function_to { # package, subname, coderef _fuck_with_glob(@_); } sub _add_variable_to { # package, varname, value _fuck_with_glob(@_); } sub _fuck_with_glob { my ($package, $variable_name, $value) = @_; die "WHOA THERE, '$value' isn't a ref" unless ref $value; no strict 'refs'; *{"${package}::${variable_name}"} = $value; } 1; __END__ =head1 NAME Package::FromData - generate a package with methods and variables from a data structure =head1 SYNOPSIS Given a data structure like this: my $packages = { 'Foo::Bar' => { constructors => ['new'], # my $foo_bar = Foo::Bar->new static_methods => { # Foo::Bar->method next_word => [ # Foo::Bar->next_word ['foo'] => 'bar', # Foo::Bar->next_word('foo') = bar ['hello'] => 'world', [qw/bar baz/] => 'baz', # Foo::Bar->next_word(qw/foo bar/) # = baz 'default_value' ], one => [ 1 ], # Foo::Bar->one = 1 }, methods => { wordify => [ '...' ], # $foo_bar->wordify = '...' # Foo::Bar->wordify = # baz always returns Foo::Bar::Baz->new baz => [ { new => 'Foo::Bar::Baz' } ], }, functions => { map_foo_bar => [ 'foo' => 'bar', 'bar' => 'foo' ], context => { scalar => 'called in scalar context', list => [qw/called in list context/], } }, variables => { '$VERSION' => '42', # $Foo::Bar::VERSION '@ISA' => ['Foo'], # @Foo::Bar::ISA '%FOO' => {Foo => 'Bar'}, # %Foo::Bar::FOO }, }, }; and some code like this: use Package::FromData; create_package_from_data($packages); create the package C and the functions as specified above. After you C, you can use C as though it were a module you wrote: my $fb = Foo::Bar->new # blessed hash reference $fb->baz # a new Foo::Bar::Baz $fb->wordify # '...' $fb->next_word('foo') # 'bar' Foo::Bar->next_word('foo') # 'bar' Foo::Bar->baz # , it's an instance method Foo::Bar::map_foo_bar('foo') # 'bar' $Foo::Bar::VERSION # '42' Not a very useful package, but you get the idea. =head1 DESCRIPTION This module creates a package with predefined methods, functions, and variables from a data structure. It's used for testing (mock objects) or experimenting. The idea is that you define a package containing functions that return values based on keys, and the rest of your app uses this somehow. (I use it so that C<< Jifty->... >> or C<< Catalyst.uri_for >> will work in templates being served via L.) =head2 THE TOP The top level data structure is a hash of package names / package definition hash pairs. =head2 PACKAGE DEFINITION HASHES Each package is defined by a package definition hash. This can contain a few keys: =head3 constructors An arrayref of constructors to be generated. The generated code looks like: sub { my $class = shift; return bless {}, $class; } =head3 functions The functions key should point to a hash of function names / function definiton array pairs. =head4 FUNCTION DEFINITION ARRAYS The function definition array is a list of pairs followed by an optional single value. The pairs are treated like a @_ => result of function hash, and the optional single element is used as a default return value. The expected input (@_) can be deep Perl data structures; an input => output pair matches if the C<\@_> in the program Cs the input rule you specify. The pairs are of the form ARRAYREF => SCALAR|ARRAYREF|SEPECIAL. To make C return C, you would add a pair like C<[ 'foo', 'bar' ] => 'baz'> to the definition hash. To return a bare list, use a arrayref; C<['foo','bar'] => ['foo','bar']>. To return a reference to a list, nest an arrayrf in the arrayref; C. To return different values in scalar or list context, pass a hash as the output definition: [ [input] => { scalar => '42', list => [qw/contents of the list/] }, ... ] To return a hashref, just say C<< [{ ... }] >>. Finally, the function definition array may be a single hash containing a C package> pair, which means to always call C<< package->method >> and return the result. This makes it possible for packages defined with C to be nested. =head3 methods Like functions, but the first argument (<$self>) is ignored. =head3 static_methods Like methods, but can be invoked against the class name instead of and instance of the class. =head3 variables A hash of variable name (including sigil) / value pairs. Keys starting with @ or % must point to the appropriate reference type. =head1 EXPORTS C =head1 FUNCTIONS =head2 create_package_from_data See L above. =head1 BUGS Probably. Report them to RT. =head1 CODE REPOSITORY The git repository is at L and can be cloned with: git clone git://git.jrock.us/Package-FromData =head1 AUTHOR Jonathan Rockway C<< >> =head1 COPYRIGHT Copyright (c) 2007, Jonathan Rockway. This module free software. You may redistribute it under the same terms as Perl itself.