#!/usr/bin/perl
use strict;
use warnings;
use Dancer::Template::Simple;
use File::Basename 'basename', 'dirname';
use File::Spec::Functions;
use Getopt::Long;
use Dancer::Renderer;
use IO::Socket::INET;
use IO::Select;
use constant FILE => 1;
# options
my $help = 0;
my $name = undef;
my $path = '.';
sub templates($);
sub app_tree($);
sub create_node($;$);
GetOptions(
"h|help" => \$help,
"a|application=s" => \$name,
"p|path=s" => \$path,
);
# main
my $PERL_INTERPRETER = -r '/usr/bin/env' ? '#!/usr/bin/env perl' : "#!$^X";
usage() and exit(0) if $help;
usage() if not defined $name;
usage() unless -d $path && -w $path;
validate_app_name($name);
my $DO_OVERWRITE_ALL = 0;
my $DANCER_APP_DIR = get_application_path($path, $name);
my $DANCER_SCRIPT = get_script_path($name);
my ($LIB_FILE, $LIB_PATH) = get_lib_path($name);
my $AUTO_RELOAD = eval "require Module::Refresh and require Clone" ? 1 : 0;
require Dancer;
my $DANCER_VERSION = $Dancer::VERSION;
version_check();
safe_mkdir($DANCER_APP_DIR);
create_node( app_tree($name), $DANCER_APP_DIR );
# subs
sub usage {
print <<'ENDUSAGE';
Dancer Helper - bootstrap a Dancer application
Usage:
dancer [options] -a <appname>
options are following :
-h, --help : display this help message
-a, --application : name of the application to create
-p, --path : existing path where to create the application tree
(current directory if not specified, must be writable)
ENDUSAGE
exit 0;
}
sub validate_app_name {
my $name = shift;
if ($name =~ /[^\w:]/ || $name =~ /^\d/ || $name =~ /\b:\b|:{3,}/) {
warn "Error: Invalid application name.\n";
usage();
}
}
sub get_application_path {
my ($path, $app_name) = @_;
catdir($path, _dash_name($app_name));
}
sub get_lib_path {
my $name = shift;
my @lib_path = split('::', $name);
my ($lib_file, $lib_path) = (pop @lib_path) . ".pm";
$lib_path = join('/', @lib_path);
return ($lib_file, $lib_path);
}
sub get_script_path {
_dash_name(shift);
}
sub _dash_name {
my $name = shift;
$name =~ s/\:\:/-/g;
$name;
}
sub create_node($;$) {
my ($node, $root) = @_;
$root ||= '.';
my $templates = templates($name);
while ( my ($path, $content) = each %$node ) {
$path = catfile($root, $path);
if (ref($content) eq 'HASH') {
safe_mkdir($path);
create_node($content, $path);
} elsif (ref($content) eq 'CODE') {
# The content is a coderef, which, given the path to the file it
# should create, will do the appropriate thing:
$content->($path);
} else {
my $file = basename($path);
my $dir = dirname($path);
my $ex = ($file =~ s/^\+//); # look for '+' flag (executable)
my $template = $templates->{$file};
$path = catfile($dir, $file); # rebuild the path without the '+' flag
write_file($path, $template, {appdir => File::Spec->rel2abs($DANCER_APP_DIR)});
chmod 0755, $path if $ex;
}
}
}
sub app_tree($) {
my ($appname) = @_;
return {
"+$DANCER_SCRIPT.pl" => FILE,
"Makefile.PL" => FILE,
lib => {
$LIB_PATH => {
$LIB_FILE => FILE,}
},
"config.yml" => FILE,
"environments" => {
"development.yml" => FILE,
"production.yml" => FILE,
},
"views" => {
"layouts" => {"main.tt" => FILE,},
"index.tt" => FILE,
},
"public" => {
"+dispatch.cgi" => FILE,
"+dispatch.fcgi" => FILE,
"404.html" => FILE,
"500.html" => FILE,
"css" => {
"style.css" => FILE,
"error.css" => FILE,
},
"images" => {},
"favicon.ico" => \&write_favicon,
},
"t" => {
"001_base.t" => FILE,
"002_index_route.t" => FILE,
},
};
}
sub safe_mkdir {
my ($dir) = @_;
if (not -d $dir) {
print "+ $dir\n";
mkdir $dir;
}
else {
print " $dir\n";
}
}
sub write_file {
my ($path, $template, $vars) = @_;
die "no template found for $path" unless defined $template;
$vars->{dancer_version} = $DANCER_VERSION;
# if file already exists, ask for confirmation
if (-f $path && (not $DO_OVERWRITE_ALL)) {
print "! $path exists, overwrite? [N/y/a]: ";
my $res = <STDIN>; chomp($res);
$DO_OVERWRITE_ALL = 1 if $res eq 'a';
return 0 unless ($res eq 'y') or ($res eq 'a');
}
my $fh;
my $content = process_template($template, $vars);
print "+ $path\n";
open $fh, '>', $path or die "unable to open file `$path' for writing: $!";
print $fh $content;
close $fh;
}
sub process_template {
my ($template, $tokens) = @_;
my $engine = Dancer::Template::Simple->new;
$engine->{start_tag} = '[%';
$engine->{stop_tag} = '%]';
return $engine->render(\$template, $tokens);
}
# Given a path, get the favicon.ico file content from the __DATA__ section and
# write it to that file.
sub write_favicon {
my $path = shift;
open(my $fh, '>', $path)
or warn "Failed to write favicon to $path - $!" and return;
my $data = do { local $/; <DATA> };
print {$fh} unpack 'u*', $data;
close $fh;
}
sub templates($) {
my $appname = shift;
return {
'Makefile.PL' =>
"use strict;
use warnings;
use ExtUtils::MakeMaker;
WriteMakefile(
NAME => '$appname',
AUTHOR => q{YOUR NAME <youremail\@example.com>},
VERSION_FROM => 'lib/$appname.pm',
ABSTRACT => 'YOUR APPLICATION ABSTRACT',
(\$ExtUtils::MakeMaker::VERSION >= 6.3002
? ('LICENSE'=> 'perl')
: ()),
PL_FILES => {},
PREREQ_PM => {
'Test::More' => 0,
'YAML' => 0,
'Dancer' => [% dancer_version %],
},
dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
clean => { FILES => '$appname-*' },
);
",
'index.tt' => '<h2>It Works!</h2>
<p>
I\'m in <code>[%appdir%]/views/index.tt</code>
</p>
',
'main.tt' =>
Dancer::Renderer->html_page($appname, '<% content %>'),
"dispatch.cgi" =>
"$PERL_INTERPRETER
use Plack::Runner;
use Dancer ':syntax';
my \$psgi = path(dirname(__FILE__), '..', '$DANCER_SCRIPT.pl');
Plack::Runner->run(\$psgi);
",
"dispatch.fcgi" =>
"$PERL_INTERPRETER
use Plack::Handler::FCGI;
use Dancer ':syntax';
my \$psgi = path(dirname(__FILE__), '..', '$DANCER_SCRIPT.pl');
my \$app = do(\$psgi);
my \$server = Plack::Handler::FCGI->new(nproc => 5, detach => 1);
\$server->run(\$app);
",
"$DANCER_SCRIPT.pl" =>
"$PERL_INTERPRETER
use Dancer;
use lib path(dirname(__FILE__), 'lib');
load_app '$appname';
dance;
",
"$LIB_FILE" =>
"package $appname;
use Dancer ':syntax';
our \$VERSION = '0.1';
get '/' => sub {
template 'index';
};
true;
",
'style.css' =>
'body {
font-family: Lucida,sans-serif;
color: #eee;
background-color: #1f1b1a;
}
#content {
color: #000;
background-color: #eee;
padding: 1em;
margin: 1em;
padding-top: 0.5em;
}
a {
color: #a5ec02;
}
h1 {
color: #a5ec02;
}
footer {
border-top: 1px solid #aba29c;
margin-top: 2em;
padding-top: 1em;
font-size: 10px;
color: #ddd;
}
pre {
font-family: \"lucida console\",\"monaco\",\"andale mono\",\"bitstream vera sans mono\",\"consolas\",monospace;
}
',
"error.css" =>
"body {
font-family: Lucida,sans-serif;
}
h1 {
color: #AA0000;
border-bottom: 1px solid #444;
}
h2 { color: #444; }
pre {
font-family: \"lucida console\",\"monaco\",\"andale mono\",\"bitstream vera sans mono\",\"consolas\",monospace;
font-size: 12px;
border-left: 2px solid #777;
padding-left: 1em;
}
footer {
font-size: 10px;
}
span.key {
color: #449;
font-weight: bold;
width: 120px;
display: inline;
}
span.value {
color: #494;
}
/* these are for the message boxes */
pre.content {
background-color: #eee;
color: #000;
padding: 1em;
margin: 0;
border: 1px solid #aaa;
border-top: 0;
margin-bottom: 1em;
}
div.title {
font-family: \"lucida console\",\"monaco\",\"andale mono\",\"bitstream vera sans mono\",\"consolas\",monospace;
font-size: 12px;
background-color: #aaa;
color: #444;
font-weight: bold;
padding: 3px;
padding-left: 10px;
}
pre.content span.nu {
color: #889;
margin-right: 10px;
}
pre.error {
background: #334;
color: #ccd;
padding: 1em;
border-top: 1px solid #000;
border-left: 1px solid #000;
border-right: 1px solid #eee;
border-bottom: 1px solid #eee;
}
",
"404.html" =>
Dancer::Renderer->html_page(
"Error 404",
'<h2>Page Not Found</h2><p>Sorry, this is the void.</p>',
'error'),
"500.html" =>
Dancer::Renderer->html_page(
"Error 500",
'<h2>Internal Server Error</h2>'
. '<p>Wooops, something went wrong</p>',
'error'),
'config.yml' =>
"layout: \"main\"
logger: \"file\"
appname: \"$name\"
",
'development.yml' =>
"log: \"core\"
warnings: 1
show_errors: 1
# auto_reload is a development feature
# you should enable it by yourself if you want it
# Module::Refresh is needed
auto_reload: 0
",
'production.yml' =>
'log: "warning"
warnings: 0
show_errors: 0
route_cache: 1
# never enable auto_reload in production
auto_reload: 0
',
"001_base.t" =>
"use Test::More tests => 1;
use strict;
use warnings;
use_ok '$appname';
",
"002_index_route.t" =>
"use Test::More tests => 3;
use strict;
use warnings;
# the order is important
use $appname;
use Dancer::Test;
route_exists [GET => '/'], 'a route handler is defined for /';
response_status_is ['GET' => '/'], 200, 'response status is 200 for /';
response_content_like [GET => '/'], qr/It Works.*I'm in.*index.tt/s,
'content looks OK for /';
",
};
}
# LWP, File::Fetch and other high level interfaces are not avaiable to us in perl 5.8.5.
sub send_http_request {
my $url = shift;
my ($host, $path);
if ($url =~ /https?:\/\/([^\/]+)(.*)/) {
($host, $path) = ($1, $2);
}
else {
die "unknown url: $url";
}
my $timeout = 2;
my $latest_version;
my $sock = IO::Socket::INET->new(PeerAddr => $host, PeerPort => 'http(80)', Proto => 'tcp');
return undef if not defined $sock;
my $req = "GET $path HTTP/1.0\x0d\x0aHost: $host\x0d\x0a\x0d\x0a";
$sock->send( $req );
my $select = IO::Select->new( $sock );
my $resp;
while ( $select->can_read( $timeout ) ) {
$resp = '';
my $ret = $sock->sysread( $resp, 4096, length($resp) );
$select->remove( $sock );
}
my @headers = split /\n/, $resp;
my $status_line = shift @headers;
# redirection handling
if ($status_line =~ /HTTP\/.*30[12]/) {
for my $header (@headers) {
if ($header =~ /Location: (\S+).*$/) {
my $path = $1;
if ($path =~ /^http:/) {
return send_http_request($path);
}
else {
return send_http_request("http://${host}${path}");
}
}
last if not $header;
}
}
return $resp;
}
sub version_check {
my $latest_version = 0;
require Dancer;
my $resp = send_http_request('http://search.cpan.org/dist/Dancer/CHANGES');
if ($resp && $resp =~ m/Dancer (\d\.\d+)/) {
$latest_version = $1;
}
if ($latest_version > $DANCER_VERSION) {
print qq|
The latest Dancer release is $latest_version, you are currently using $DANCER_VERSION.
Please check http://search.cpan.org/dist/Dancer/ for updates.
|;
}
}
=pod
=head1 NAME
dancer - helper script to create new Dancer applications
=head1 DESCRIPTION
Helper script for providing a bootstrapping method to quickly and easily create
the framework for a new Dancer application.
=head1 USAGE
dancer [options] -a <appname>
=over
=item -h, --help : print what you are currently reading
=item -a, --application : the name of your application
=item -p, --path : the path where to create your application
(current directory if not specified)
=back
=head1 EXAMPLE
Here is an application created with dancer:
$ dancer -a mywebapp
+ mywebapp
+ mywebapp/config.yml
+ mywebapp/views
+ mywebapp/views/layouts
+ mywebapp/views/layouts/main.tt
+ mywebapp/views/index.tt
+ mywebapp/environments
+ mywebapp/environments/production.yml
+ mywebapp/environments/development.yml
+ mywebapp/mywebapp.pm
+ mywebapp/mywebapp.pl
+ mywebapp/favicon.ico
The application is ready to serve:
$ cd mywebapp
$ ./mywebapp.pl
>> Listening on 127.0.0.1:3000
== Entering the development dance floor ...
=head1 AUTHOR
This script has been written by Sebastien Deseille
<sebastien.deseille@gmail.com> and Alexis Sukrieh
<sukria@cpan.org>.
=head1 SOURCE CODE
See L<Dancer> for more information.
=head1 LICENSE
This module is free software and is published under the same
terms as Perl itself.
=cut
# The following ugly data is the default favicon.ico file to write; uuencoded
# using pack 'u*'.
__DATA__
M```!``$`$!````$`"`!H!0``%@```"@````0````(`````$`"```````````
M``````````````````````!D12\`:4LV`&I,-P!K3CD`;$XY`&Q/.@!M3SH`
M;5`Z`&U0.P!N4#L`;5`\`&U1.P!N43L`;E$\`&Y1/0!N4CT`;U(]`&]2/@!O
M4SX`<E9!`')60@!S6$0`=UQ(`'E>2@!Y8$P`>F!-`'QB3@!]8U``?611`(!G
M5`"`:%4`A&M:`(9M7`"+=&,`C79E`)!Z:0"2?6P`KIZ2`+"AE0"\L*8`P[>M
M`,S"NP#/Q[\`T\O$`-7-QP#P[^L`]_;U`/K[^0#]_OT`____````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-
M"`T-#0T-#0T-#0T-#0T2&@@-#0T-#0T-#0T-#0T!#QD(#0T-#0T-#0T("`<`
M)08-#0T-#0T(#0@-#1$F(04-"`T-#0T-#0,-#0T-%AL7"@P-#0T-#0<=$`T0
M(B0>%!`+"0T-#0T(%0@-!"`L+B\H#@@(#0T-"`T(#0T(!0@8,"LC'PT(#0T-
M#0T-#0T("`,G,2T'"`@-#0T-#0T-#0T-`API*@T-#0T-#0T-#0T-#0T)"`(3
M"`T-#0T-#0T-#0T-#0@-"`T-#0T-#0T-#0T-#0T-#0T-#0T-#0``````````
M````````````````````````````````````````````````````````````
+````````````````