File Coverage

blib/lib/DSL/Tiny/Role.pm
Criterion Covered Total %
statement 19 19 100.0
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 26 100.0


line stmt bran cond sub pod time code
1             ## no critic (RequireUseStrict RequireUseWarnings)
2             package DSL::Tiny::Role;
3             ## critic
4             # ABSTRACT: A simple yet powerful DSL builder.
5              
6 3     3   299853 use Moo::Role;
  3         7  
  3         23  
7              
8 3     3   1431 use Sub::Exporter -setup => { groups => { install_dsl => \&_dsl_build, } };
  3         8  
  3         52  
9              
10 3     3   1547 use Data::OptList;
  3         7  
  3         29  
11 3     3   3081 use MooX::Types::MooseLike::Base qw(ArrayRef);
  3         26015  
  3         1863  
12 3     3   30 use Params::Util qw(_ARRAYLIKE);
  3         6  
  3         246  
13 3     3   18 use Sub::Exporter::Util qw(curry_method);
  3         4  
  3         32  
14             {
15             $DSL::Tiny::Role::VERSION = '0.001';
16             }
17              
18 3     3   1491 BEGIN { *install_dsl = \&import; }
19              
20             has dsl_keywords => (
21             is => 'rw',
22             isa => ArrayRef,
23             lazy => 1,
24             builder => 'build_dsl_keywords',
25             );
26              
27             sub _dsl_build {
28             my ( $invocant, $group, $arg, $col ) = @_;
29              
30             # if not already an instance, create one.
31             my $instance = ref $invocant ? $invocant : $invocant->new();
32              
33             # fluff up the keyword specification
34             my $keywords = Data::OptList::mkopt_hash( $instance->dsl_keywords,
35             { moniker => 'keyword list' }, ['HASH'], );
36              
37             my %dsl = map { $_ => $instance->_compile_keyword( $_, $keywords->{$_} ) }
38             keys $keywords;
39              
40             return \%dsl;
41             }
42              
43             sub _compile_keyword {
44             my ( $self, $keyword, $args ) = @_;
45              
46             # generate code for keyword
47             my $code_generator = $args->{as} || curry_method($keyword);
48             my $code = $code_generator->( $self, $keyword );
49              
50             # generate before code, if any
51             # make sure before is an array ref
52             # call each generator (if any), save resulting coderefs
53             my $before = $args->{before};
54             $before = [$before] unless _ARRAYLIKE($before);
55             my @before_code = map { $_->($self) } grep { defined $_ } @{$before};
56              
57             # generate after code, if any
58             my $after = $args->{after};
59             $after = [$after] unless _ARRAYLIKE($after);
60             my @after_code = map { $_->($self) } grep { defined $_ } @{$after};
61              
62             if ( @before_code or @after_code ) {
63             my $new_code = sub {
64             my @rval;
65              
66             $_->(@_) for @before_code;
67              
68             # Cribbed from $Class::MOP::Method::Wrapped::_build_wrapped_method
69             # not sure that it doesn't have more parens then necessary, but
70             # if it works for them...
71             ( ( defined wantarray )
72             ? ( (wantarray)
73             ? ( @rval = $code->(@_) )
74             : ( $rval[0] = $code->(@_) )
75             )
76             : $code->(@_)
77             );
78              
79             $_->(@_) for @after_code;
80              
81             return unless defined wantarray;
82             return wantarray ? @rval : $rval[0];
83             };
84             return $new_code;
85             }
86              
87             return $code;
88             }
89              
90             1;
91              
92             __END__
93              
94             =pod
95              
96             =head1 NAME
97              
98             DSL::Tiny::Role - A simple yet powerful DSL builder.
99              
100             =head1 VERSION
101              
102             version 0.001
103              
104             =head1 SYNOPSIS
105              
106             # In e.g. MooseDSL.pm, describe a simple DSL.
107             package MooseDSL;
108              
109             use Moose; # or use Moo;
110              
111             with qw(DSL::Tiny::Role);
112              
113             sub build_dsl_keywords {
114             return [
115             # keywords will be run through curry_method
116             qw(argulator return_self clear_call_log),
117             ];
118             }
119              
120             has call_log => (
121             clearer => 'clear_call_log',
122             default => sub { [] },
123             is => 'rw',
124             lazy => 1
125             );
126              
127             sub argulator {
128             my $self = shift;
129             push @{ $self->call_log }, join "::", @_;
130             }
131              
132             sub return_self { return $_[0] }
133              
134             1;
135              
136             ################################################################
137              
138             # and then in another file you can use that DSL
139              
140             use Test::More;
141             use Test::Deep;
142              
143             use MooseDSL qw( -install_dsl );
144              
145             # peek under the covers, get the instance
146             my $dsl = return_self;
147             isa_ok( $dsl, 'MooseDSL' );
148              
149             # test argument handling, single scalar
150             argulator("a scalar");
151             cmp_deeply( $dsl->call_log, ['a scalar'], 'scalar arg works' );
152             clear_call_log;
153              
154             # test argument handling, list of args
155             argulator(qw(a list of things));
156             cmp_deeply( $dsl->call_log, ['a::list::of::things'], 'list arg works' );
157             clear_call_log;
158              
159             done_testing;
160              
161             =head1 DESCRIPTION
162              
163             I<This is an initial release. It's all subject to rethinking. Comments
164             welcome.>
165              
166             every time a language advertises "we make writing dsls easy!" i
167             read "i'm going to have to learn a new language for every project"
168              
169             Jesse Luehrs (@doyster) 3/8/13, 12:11 PM
170              
171             Domain-specific languages (DSL's) aid in the efficient expression of
172             configurations, problems and solutions within a particular domain. While some
173             DSL's are built from the ground up with custom lexers, parsers,
174             etc... (e.g. the Unix build tool "make"), other "internal DSL's" (L<Werner
175             Schuster|http://www.infoq.com/news/2007/06/dsl-or-not>) are distilled from
176             existing languages and "speak the language of their domain with an accent"
177             (L<Piers Cawley|http://www.bofh.org.uk/2007/05/19/domain-agnostic-languages>).
178              
179             A variety of Perl tools and libraries sport domain specific langagues,
180             e.g. L<Dancer>, L<Module-CPANfile> and L<Module-Install> and the number of
181             re-implementations of the underlying plumbing is almost exactly equal to the
182             number of such modules. These implementations usually devolve into dirty
183             tricks (e.g. explicit package stash manipulations) and re-invention of several
184             wheels.
185              
186             L<DSL::Tiny> packages the common functionality required to implement an
187             internal DSL, building on powerful foundations (L<Sub::Exporter>) and effective
188             techniques (L<Moose> and L<Moo> roles) to allow developers to focus on their
189             domain-specific issues. It builds on a flexible mechanism for exporting a set
190             of subroutines into a package; provides a consistent framework for subroutine
191             currying; and automates the construction of instances, their association with
192             DSL fragments and the evaluation of those fragments.
193              
194             In other words, when I needed to build an internal DSL for a project, I was
195             shocked at how often the basic brushstrokes had been repeated and how often
196             these implementations dug down and peeked underneath Perl's stashes. These
197             modules are my attempt to provide a reusable solution to the problem via
198             existing high-leverage tools.
199              
200             =head1 ATTRIBUTES
201              
202             =head2 dsl_keywords
203              
204             Returns an arrayref of dsl keyword info.
205              
206             It is lazy. Classes which consume the role are required to supply a builder
207             named C<_build_dsl_keywords>.
208              
209             In its canonical form the contents of the array reference are a series of array
210             references containing keyword_name => { option_hash } pairs, e.g.
211              
212             [ [ keyword1 => { as => &generator('method1') } ],
213             [ keyword2 => { before => &generator ]
214             ]
215              
216             Generators are as described in the L<Sub::Exporter> documentation.
217              
218             However, as the contents of this array reference are processed with
219             Data::OptList there is a great deal of flexibility, e.g.
220              
221             [ qw( m1 m2 ), k4 => { as => &generator('some_method' } ]
222              
223             is equivalent to:
224              
225             [ m1 => undef, m2 => undef, k4 => { as => generator('some_method') } ]
226              
227             Options are optional. In particular, if no C<as> generator is provided then
228             the keyword name is presumed to also be the name of a method in the class and
229             C<Sub::Exporter::Utils::curry_method> will be applied to that method to
230             generate the coderef for that keyword. The makes the above equivalent to:
231              
232             [ m1 => { as => generator('m1') }, m2 => { as => generator('m2') },
233             k4 => { as => generator('some_method') }
234             ]
235              
236             In its simplest form, the keyword arrayref contains a list of method names
237             relative to class which consumes this role.
238              
239             [ qw( m1 m2 ) ]
240              
241             Supported options include:
242              
243             =over 4
244              
245             =item as
246              
247             =item before
248              
249             =item after
250              
251             =back
252              
253             =head1 METHODS
254              
255             =head2 import
256              
257             An import routine generated by Sub::Exporter.
258              
259             When invoked as a class method (usually via C<use>) with a C<-install_dsl>
260             argument it will construct a new instance then generate and install a set of
261             subroutines using the information provided in the instance's C<dsl_keywords>
262             attribute.
263              
264             TODO.
265              
266             =head2 install_dsl
267              
268             A synonym for the Sub::Exporter generated import method. Sounds better when
269             one uses it to install into an instance.
270              
271             =head2 _dsl_build
272              
273             Private-ish. Do you really want to be here?
274              
275             C<_dsl_build> build's up the set of keywords that L<Sub::Exporter> will
276             install.
277              
278             It returns a hashref whose keys are names of keywords and whose values are
279             coderefs implementing the respective behavior.
280              
281             It can be invoked on a class (a.k.a. as a class method), usually by C<use>. If
282             so, a new instance of the class will be constructed and the various keywords
283             are curried with respect to that instance.
284              
285             It can be invoked on a class instance, e.g. via an explicit invocation of
286             L<import> on an instance. If so, then that instance is used when constructing
287             the keywords.
288              
289             =head2 _compile_keyword
290              
291             Private, go away.
292              
293             Generate a sub that implements the keyword, taking care of before's and afters.
294              
295             =head1 REQUIRES
296              
297             =head2 build_dsl_keywords
298              
299             A subroutine, used as the Moo{,se} builder for the L</dsl_keywords> attribute.
300             It returns an array reference containing information about the methods and
301             subroutines that implement the keywords in the DSL.
302              
303             =head1 AUTHOR
304              
305             George Hartzell <hartzell@alerce.com>
306              
307             =head1 COPYRIGHT AND LICENSE
308              
309             This software is copyright (c) 2013 by George Hartzell.
310              
311             This is free software; you can redistribute it and/or modify it under
312             the same terms as the Perl 5 programming language system itself.
313              
314             =cut