File Coverage

blib/lib/Perinci/Sub/Gen/FromClass.pm
Criterion Covered Total %
statement 120 130 92.3
branch 17 30 56.6
condition 7 13 53.8
subroutine 21 22 95.4
pod 1 1 100.0
total 166 196 84.6


line stmt bran cond sub pod time code
1             package Perinci::Sub::Gen::FromClass;
2              
3             our $DATE = '2014-08-04'; # DATE
4             our $VERSION = '0.02'; # VERSION
5              
6 1     1   4876 use 5.010001;
  1         3  
  1         38  
7 1     1   5 use strict;
  1         1  
  1         29  
8 1     1   5 use warnings;
  1         1  
  1         29  
9              
10 1     1   968 use Monkey::Patch::Action qw(patch_package);
  1         4337  
  1         57  
11 1     1   854 use Perinci::Sub::Gen;
  1         159  
  1         526  
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw(gen_func_from_class);
16              
17             our %SPEC;
18              
19             $SPEC{gen_func_from_class} = {
20             v => 1.1,
21             summary => 'Generate function from a class',
22             description => <<'_',
23              
24             `gen_func_from_class` will create a function and Rinci metadata from a
25             {Mo,Moo,Moose,Mouse} class. Given a class like this:
26              
27             # MyClass
28             use Moo;
29             has attr1 => (is => 'ro', required=>1);
30             has attr2 => (is => 'rw');
31             sub meth1 { ... }
32             sub meth2 { ... }
33             1;
34              
35             will create a function that does something like this (it will basically
36             instantiate a class, set its attributes, and call a method):
37              
38             MyClass->new(attr1=>..., attr2=>...)->meth1;
39              
40             along with Rinci metadata like this:
41              
42             {
43             v => 1.1,
44             args => {
45             attr1 => { req=>1, schema=>'any' },
46             attr2 => { schema=>'any' },
47             },
48             }
49              
50             Currently only Mo- and Moo-based class is supported. Support for other Mo*
51             family members will be added.
52              
53             _
54             args => {
55             %Perinci::Sub::Gen::common_args,
56             class => {
57             summary => 'Class name, will be loaded with require()',
58             req => 1,
59             },
60             method => {
61             summary => 'Method of class to call',
62             req => 1,
63             # XXX guess if not specified?
64             },
65             method_args => {
66             schema => 'array*',
67             },
68             },
69             result => {
70             summary => 'A hash containing generated function, metadata',
71             schema => 'hash*',
72             },
73             };
74             sub gen_func_from_class {
75 2     2 1 3125 my %args = @_;
76              
77 2 50       11 my $class = $args{class} or return [400, "Please specify 'class'"];
78 2 50       16 $class =~ /\A\w+(::\w+)*\z/ or
79             return [400, "Invalid value for 'class', please use Foo::Bar ".
80             "syntax only"];
81 2 50       11 my $method = $args{method} or return [400, "Please specify 'method'"];
82              
83 2         4 my %mo_attrs;
84             {
85 2         3 my $handle_mo;
  2         3  
86             # doesn't work if Mo is inlined
87 2 50       138 if (eval "require Mo; 1") {
88 2         1038 require Mo::default;
89 2         931 require Mo::required;
90 2         166 my $M = "Mo::";
91             # copied and modified from Mo 0.38
92             $handle_mo = patch_package(
93             'Mo', 'import', 'replace',
94             sub {
95 1     1   7 no strict; ###
  1         2  
  1         582  
96 2     2   33 import warnings;
97 2         4 $^H |= 1538;
98 2         7 my ( $P, %e, %o ) = caller . '::';
99 2         19 shift;
100 1     1   5 eval "no Mo::$_", &{ $M . $_ . '::e' }( $P, \%e, \%o, \@_ ) for @_;
  1     1   2  
  1     1   18  
  1     1   68  
  1         2  
  1         16  
  1         5  
  1         1  
  1         13  
  1         74  
  1         2  
  1         13  
  2         129  
  4         17  
101 2 50       86 return if $e{M};
102             %e = ( 'extends',
103 1     1   432 sub { eval "no $_[0]()"; @{ $P . ISA } = $_[0] },
  1     1   37  
  1         10  
  1         64  
  1         2  
  1         18  
104             'has',
105             sub {
106 3     3   13 my $n = shift;
107 3         5 my $p = $P; $p =~ s/::$//; $mo_attrs{$p}{$n} = {@_}; ###
  3         13  
  3         11  
108 3 0   0   11 my $m = sub { $#_ ? $_[0]{$n} = $_[1] : $_[0]{$n} };
  0         0  
109 3 50       8 @_ = ( 'default', @_ ) if !( $#_ % 2 );
110 3         18 $m = $o{$_}->( $m, $n, @_ ) for sort keys %o;
111 3         75 *{ $P . $n } = $m;
  3         18  
112             },
113 2         18 %e,
114             );
115 2         7 *{ $P . $_ } = $e{$_} for keys %e;
  4         21  
116 2         5 @{ $P . ISA } = $M . Object;
  2         219  
117             },
118 2         22 );
119             }
120             # to support Mouse and Moose we'll need to let user enable it, because
121             # of the startup overhead
122 2         139 my $classp = $class;
123 2         9 $classp =~ s!::!/!g; $classp .= ".pm";
  2         5  
124 2         1499 require $classp;
125             }
126              
127 2   50     79428 my $install = $args{install} // 1;
128              
129 2   50     15 my $fqname = $args{name} // 'noname';
130 2 50 33     10 return [400, "Please specify 'name'"] unless $fqname || !$install;
131 2         9 my @caller = caller();
132 2         4 my ($package, $uqname);
133 2 50       11 if ($fqname =~ /(.+)::(.+)/) {
134 0         0 $package = $1;
135 0         0 $uqname = $2;
136             } else {
137 2   33     10 $package = $args{package} // $caller[0];
138 2         4 $uqname = $fqname;
139 2         6 $fqname = "$package\::$uqname";
140             }
141              
142 2         4 my %func_args;
143             {
144 2         4 my $doit;
  2         3  
145             $doit = sub {
146 1     1   5 no strict 'refs';
  1         1  
  1         259  
147 6     6   15 my $pkg = shift;
148 6   100     41 my $ass = $mo_attrs{$pkg} //
149             $Moo::MAKERS{$pkg}{constructor}{attribute_specs};
150 6 100       15 if ($ass) {
151 4         11 for my $k (keys %$ass) {
152 8         13 my $v = $ass->{$k};
153 8 100       30 my $as = {
154             req => $v->{required} ? 1:0,
155             };
156 8 100       23 if (exists $v->{default}) {
157 2 50       7 if (ref($v->{default}) eq 'CODE') {
158 0         0 $as->{default} = $v->{default}->();
159             } else {
160 2         5 $as->{default} = $v->{default};
161             }
162             }
163 8         25 $func_args{$k} = $as;
164             }
165             }
166 6         12 $doit->($_) for @{"$pkg\::ISA"};
  6         50  
167 2         13 };
168 2         7 $doit->($class);
169             }
170              
171 2         18 my $meta = {
172             v => 1.1,
173             (summary => $args{summary}) x !!$args{summary},
174             (description => $args{description}) x !!$args{description},
175             args => \%func_args,
176             result_naked => 1,
177             };
178              
179             my $func = sub {
180 1     1   5 no strict 'refs';
  1         2  
  1         91  
181 2     2   4978 my %func_args = @_;
182 2         20 my $obj = $class->new(%func_args);
183 2         1380 my @meth_args;
184 2 50       18 if ($args{method_args}) {
185 0         0 @meth_args = @{ $args{method_args} };
  0         0  
186             }
187 2         18 $obj->$method(@meth_args);
188 2         11 };
189              
190 2 50       8 if ($install) {
191 1     1   4 no strict 'refs';
  1         2  
  1         27  
192 1     1   12 no warnings;
  1         2  
  1         112  
193             #$log->tracef("Installing function as %s ...", $fqname);
194 0         0 *{ $fqname } = $func;
  0         0  
195 0         0 ${$package . "::SPEC"}{$uqname} = $meta;
  0         0  
196             }
197              
198 2         16 return [200, "OK", {meta=>$meta, func=>$func}];
199             }
200              
201             1;
202             # ABSTRACT: Generate function (and its Rinci metadata) from a class
203              
204             __END__
205              
206             =pod
207              
208             =encoding UTF-8
209              
210             =head1 NAME
211              
212             Perinci::Sub::Gen::FromClass - Generate function (and its Rinci metadata) from a class
213              
214             =head1 VERSION
215              
216             This document describes version 0.02 of Perinci::Sub::Gen::FromClass (from Perl distribution Perinci-Sub-Gen-FromClass), released on 2014-08-04.
217              
218             =head1 SYNOPSIS
219              
220             Given a Mo/Moo/Mouse/Moose class:
221              
222             # MyClass
223             use Moo;
224             has attr1 => (is => 'ro', required=>1);
225             has attr2 => (is => 'rw');
226             sub do_this { ... }
227             sub do_that { ... }
228             1;
229              
230             you can generate a function for it:
231              
232             use Perinci::Sub::Gen::FromClass qw(gen_func_from_class);
233             gen_func_from_class(
234             name => 'do_this',
235              
236             class => 'MyClass',
237             type => 'Moo',
238             method => 'do_this',
239             method_args => [3, 4, 5], # optional
240             );
241              
242             then if you call this function:
243              
244             do_this(attr1=>1, attr2=>2);
245              
246             it will do something like (instantiate class and call a method):
247              
248             MyClass->new(attr1=>1, attr2=>2)->do_this(3, 4, 5);
249              
250             =head1 DESCRIPTION
251              
252             Sometimes some module annoyingly only provides OO interface like:
253              
254             my $obj = Foo->new(arg1=>1, arg2=>2);
255             $obj->some_action;
256              
257             when it could very well just be:
258              
259             some_action(arg1=>1, arg2=>2);
260              
261             This module helps you create that function from a class.
262              
263             =head1 FUNCTIONS
264              
265              
266             =head2 gen_func_from_class(%args) -> [status, msg, result, meta]
267              
268             Generate function from a class.
269              
270             C<gen_func_from_class> will create a function and Rinci metadata from a
271             {Mo,Moo,Moose,Mouse} class. Given a class like this:
272              
273             # MyClass
274             use Moo;
275             has attr1 => (is => 'ro', required=>1);
276             has attr2 => (is => 'rw');
277             sub meth1 { ... }
278             sub meth2 { ... }
279             1;
280              
281             will create a function that does something like this (it will basically
282             instantiate a class, set its attributes, and call a method):
283              
284             MyClass->new(attr1=>..., attr2=>...)->meth1;
285              
286             along with Rinci metadata like this:
287              
288             {
289             v => 1.1,
290             args => {
291             attr1 => { req=>1, schema=>'any' },
292             attr2 => { schema=>'any' },
293             },
294             }
295              
296             Currently only Mo- and Moo-based class is supported. Support for other Mo*
297             family members will be added.
298              
299             Arguments ('*' denotes required arguments):
300              
301             =over 4
302              
303             =item * B<class>* => I<any>
304              
305             Class name, will be loaded with require().
306              
307             =item * B<description> => I<str>
308              
309             Generated function's description.
310              
311             =item * B<install> => I<bool> (default: 1)
312              
313             Whether to install generated function (and metadata).
314              
315             By default, generated function will be installed to the specified (or caller's)
316             package, as well as its generated metadata into %SPEC. Set this argument to
317             false to skip installing.
318              
319             =item * B<method>* => I<any>
320              
321             Method of class to call.
322              
323             =item * B<method_args> => I<array>
324              
325             =item * B<name> => I<str>
326              
327             Generated function's name, e.g. `myfunc`.
328              
329             =item * B<package> => I<str>
330              
331             Generated function's package, e.g. `My::Package`.
332              
333             This is needed mostly for installing the function. You usually don't need to
334             supply this if you set C<install> to false.
335              
336             If not specified, caller's package will be used by default.
337              
338             =item * B<summary> => I<str>
339              
340             Generated function's summary.
341              
342             =back
343              
344             Return value:
345              
346             Returns an enveloped result (an array).
347              
348             First element (status) is an integer containing HTTP status code
349             (200 means OK, 4xx caller error, 5xx function error). Second element
350             (msg) is a string containing error message, or 'OK' if status is
351             200. Third element (result) is optional, the actual result. Fourth
352             element (meta) is called result metadata and is optional, a hash
353             that contains extra information.
354              
355             A hash containing generated function, metadata (hash)
356              
357             =head1 TODO
358              
359             Translate C<isa> option in C<has> into argument schema.
360              
361             =head1 SEE ALSO
362              
363             L<Rinci>
364              
365             =head1 HOMEPAGE
366              
367             Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Sub-Gen-FromClass>.
368              
369             =head1 SOURCE
370              
371             Source repository is at L<https://github.com/sharyanto/perl-Perinci-Sub-Gen-FromClass>.
372              
373             =head1 BUGS
374              
375             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Sub-Gen-FromClass>
376              
377             When submitting a bug or request, please include a test-file or a
378             patch to an existing test-file that illustrates the bug or desired
379             feature.
380              
381             =head1 AUTHOR
382              
383             Steven Haryanto <stevenharyanto@gmail.com>
384              
385             =head1 COPYRIGHT AND LICENSE
386              
387             This software is copyright (c) 2014 by Steven Haryanto.
388              
389             This is free software; you can redistribute it and/or modify it under
390             the same terms as the Perl 5 programming language system itself.
391              
392             =cut