Dancer2-0.205002000755001751001751 013171470520 12631 5ustar00jasonjason000000000000Changes100644001751001751 17500213171470520 14252 0ustar00jasonjason000000000000Dancer2-0.2050020.205002 2017-10-17 16:08:25-05:00 America/Chicago [ BUG FIXES ] * GH #1362: Make cookies http_only by default (David Precious) * GH #1366: Use proper shebang on dancer script and make EU::MM do the job * GH #1373: Unset Dancer environment vars before testing (Alberto Simões) * GH #1380: Consider class of error displayed when using show_errors (Nick Tonkin). * GH #1383: Remove Deflater from default app skeleton (Pierre Vigier) * GH #1385: Fix links inside the documentation (Alberto Simões) * GH #1390: Honour no_server_tokens config in error responses (Russell @veryrusty Jenkins) [ DOCUMENTATION ] * GH #1285: Add "Default Template Variables" section to manual (simbabque) * GH #1312: Fix docs for Dancer2::Core::Route->match, which takes a request object (simbabque). * GH #1368: Don't allow XSS in tutorial (simbabque) * GH #1383: Remove full URL on links to third party modules (Alberto Simoes) * GH #1395: Customize TT behavior via subclassing (simbabque). 0.205001 2017-07-11 08:03:21-05:00 America/Chicago [ BUG FIXES ] * GH #1332: Add check for old version of HTTP::XSCookies (Peter Mottram - SysPete) * GH #1336: Fix warnings on 5.10 and below. (Sawyer X) * GH #1347: Add Perl versions 5.22-5.26 and appveyor to Travis-CI configuration (Dave Jacoby) [ ENHANCEMENTS ] * GH #1281: Use Ref::Util in Core for all reference checks (Mickey Nasriachi) * GH #1338: Add message explaining how to run newly-created application (Jonathan Cast) [ DOCUMENTATION ] * GH #1334: Fix prefix example in Cookbook (Abdullah Diab) * GH #1335: Add missing word in request->host docs (Glenn Fowler) * GH #1337: Fix link in SEE ALSO section of Dancer2::Core::Types (Stefan Hornburg - Racke) * GH #1341: Clarify plugin documentation (Stefan Hornburg - Racke) * GH #1345, #1351, #1356: Fix password check code example in tutorial (Jonathan Cast) * GH #1355: Fix typo (Gregor Herrmann) 0.205000 2017-03-10 15:37:52-06:00 America/Chicago [ BUG FIXES ] * GH #1325: Support multi-value cookies when using HTTP::XSCookies. (James Raspass) * GH #1303: Read configuration options when send_as() creates a new serializer (Paul Williams) * GH #1290: Properly check buffer length in _read_to_end() (Marketa Wachtlova) * GH #1322: Deprecate broken request->dispatch_path in favor of request->path. Warn the developer of the deprecation (Russell @veryrusty Jenkins). [ ENHANCEMENTS ] * GH #1326: Speed up by using Type::Tiny, again. (Pete SysPete Mottram) * GH #1318: Add support for the SameSite cookie attribute. (James Raspass) * GH #1283: Skeleton now provides an example of setting the appdir. (Jason Lewis) * GH #1315: Adjust dist.ini to set "build_requires" for ExtUtils::MakeMaker. (Atoomic) * GH #1331: Preliminary prepare_app() work (Sawyer X) [ DOCUMENTATION ] * GH #1324: Fix broken link to send_file. (Fabrice Gabolde) * GH #1311: Typo and link fixes. (Breno G. de Oliveira - @garu) * GH #1310: Document query string parameters in uri_for. (Michael J South) * GH #1329: Remove dead code from file upload example (Stefan Hornburg - Racke) * GH #1256: Additions to migration manual (Daniel Perrett) * GH #1330: Add middleware examples to scaffolder (David - sbts) 0.204004 2017-01-26 18:29:34+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #1307: Fix breakage of Template::Toolkit, caused by previous release. (Peter SysPete Mottram) 0.204003 2017-01-25 15:21:40-06:00 America/Chicago [ BUG FIXES ] * GH #1299: Fix missing CPANTS prereqs (Mohammad S. Anwar) [ ENHANCEMENTS ] * GH #1249: Improve consistency with Template::Toolkit, using correct case for 'include_path', 'stop_tag', 'end_tag', and 'start_tag', removing ANYCASE option. (Klaus Ita) * Call route exception hook before logging an error, allowing devs to raise their own errors bedore D2 logging takes over. (Andy Beverley) [ DOCUMENTATION ] * Add another example of the delayed asynchronous mechanism (Ed @mohawk2 J., Sawyer X) * GH #1291: Document 'change_session_id' in Dancer2::Core::App. (Peter SysPete Mottram) * Fix typo in Dancer2::Core::Response (Gregorr Herrmann) * Document Dancer2::Plugin::RootURIFor (Mario Zieschang) 0.204002 2016-12-21 15:40:02-06:00 America/Chicago [ BUG FIXES ] * GH #975: Fix "public_dir" configuration to work, just like DANCER_PUBLIC. (Sawyer X) [ ENHANCEMENTS ] * You can now call '$self->find_plugin(...)' within a plugin in order to find a plugin, in order to use its DSL in your custom plugin. (Sawyer X) [ DOCUMENTATION ] * GH #1282: Typo in Cookbook. (Kurt Edmiston) * GH #1214: Update Migration document. (Sawyer X) * GH #1286: Clarify hook behavior when disabling layout (biafra) * GH #1280: Update documentation to use specific parameter keywords (Hunter McMillen) 0.204001 2016-10-17 08:29:00-05:00 America/Chicago [ BUG FIXES ] * Restore 5.8 support (fix test which required captures). (Russell @veryrusty Jenkins) * PR #1271: fix wrong regex check against $_ (Mickey Nasriachi) [ ENHANCEMENTS ] * GH #1262: Add 'encode_json' and 'decode_json' DSL, which are recommended instead of 'to_json' and 'from_json'. (Dennis @episodeiv lichtenthäler) [ DOCUMENTATION ] * Fix some typos.(Dennis @episodeiv lichtenthäler) * GH #1031: Remove D2::Core::Context remnants from docs. (Sawyer X) [ PACKAGING ] * GH #1273: Do not require Test::Perl::Critic to install. (Dennis lichtenthäler) 0.204000 2016-10-10 20:56:51-05:00 America/Chicago [ BUG FIXES ] * GH #1255: Fix hook overriding in plugin. (Yves Orton) * GH #1191: Named capture prior to dispatch breaks dispatch. (Yves Orton) * GH #1235: Clean up descriptions for HTTP codes 303 and 305. (Yanick Champoux) * Remove duplicate (and errornous) 451 error message. (Sawyer X) * GH #1116, #1245: Ensure cached Hash::MultiValue parameters are cloned into the new request. (Russell @veryrusty Jenkins) [ ENHANCEMENTS ] * You can now provide a $EVAL_SHIM to Dancer2::Core::App in order to have custom code run on eval{} calls. One example of this is to handle proper counting of stack frames when you want to unwind/unroll the stack for custom error reporting. (Yves Orton) * Added a cpanfile to allow installing local dependencies with carton. (Mickey Nasriachi) * GH #1260: Specify optional charset to send_file and send_as (Russell @veryrusty Jenkins) * PR #1162: Change skeleton template tags so skeletons can generate applications that use Template Toolkit default tags (Jason Lewis) * GH #1149: Fix config loading inconsistencies, support local config files in addition to standard Dancer conf files (Jonathan Scott Duff) * PR #1269: Stash decoded body_parameters separately from those in Plack::Request (Russell @veryrusty Jenkins) * GH #1253: Static middleware should send 304 Not Modified to enable intermediate level caching. (Russell @veryrusty Jenkins) [ DOCUMENTATION ] * GH #608: Remove extra general COPYRIGHT notice in Tutorial. (Sawyer X) * Simplify upload example. (Alberto Simões, Sawyer X) 0.203001 2016-09-03 20:59:47-05:00 America/Chicago [ BUG FIXES ] * GH #1237: Specify minimum version of List::Util required for pair* functionals. (Russell @veryrusty Jenkins) [ ENHANCEMENTS ] * PR #1242: Replace Class::Load with Module::Runtime (Russell Jenkins - @veryrusty) 0.203000 2016-08-24 22:09:56-05:00 America/Chicago [ BUG FIXES ] * GH #1232: Force deserialization of body data even when an existing Plack::Request object has already parsed request body. Don't double decode deserialized data. (Russell Jenkins - @veryrusty) [ ENHANCEMENTS ] * GH #1195: Add change_session_id() method - both as a good security practice and to comply with other established security standards. (Peter Mottram) * GH #1234: Add convenience functions to access Dancer's HTTP_CODES table. (Yanick Champoux) [ DOCUMENTATION ] * Fix Typo (Stefan Hornburg - Racke) * Document $session->data (Stefan Hornburg - Racke) 0.202000 2016-08-13 13:50:30-05:00 America/Chicago [ BUG FIXES ] * Fix memory leak in plugins. (Sawyer X) * GH #1180, #1220: Revert (most of) GH #1120. Change back to using MooX::Types::MooseLike until issues around Type::Tiny are resolved. Peter (@SysPete) Mottram * GH #1192: Decode body|query|request_parameters (Peter Mottram) * GH #1224: Plugins defined with :PluginKeyword attribute are now exported. (Yanick Champoux) * GH #1226: Plugins can now call the DSL of the app via $self->dsl (Sawyer X) [ ENHANCEMENTS ] * PR #1223: Add YAML::XS to Recommends (Peter Mottram) * PR #1117: If installed, use HTTP::XSCookies and all cookie operations will be faster (Peter Mottram) * PR #1228: Allow register_plugin() to pass @_ properly (Sawyer X) * PR #1231: Plugins can now call the syntax of plugins they loaded (Sawyer X) [ DOCUMENTATION ] * PR #1151: Note that config is immutable after first read (Peter Mottram) * PR #1222: Update list of files generated by `dancer2 -a`, make name of sample app consistent (Daniel Perrett) 0.201000 2016-07-22 08:26:18-05:00 America/Chicago [ BUG FIXES ] * GH #1216: Make DSL work in edge-case of plugins calling DSL before the app class loaded Dancer2. (Sawyer X) * GH #1210: Show proper module/line number in log output (Masaaki Saito) [ ENHANCEMENTS ] * GH #900: Switch from to_json to encode/encode_json (Nuno Ramos Carvalho) * GH #1196: Move serializer from JSON to JSON::MaybeXS (Nuno Ramos Carvalho) * GH #1215: Remove unused DANCER2_SHARE_DIR env variable (Jason A. Crome) [ DOCUMENTATION ] * PR #1213: Clarify params merging docs and related examples (Daniel Perrett) * Add Peter Mottram (@SysPete) to list of core developers. (Russell Jenkins) * PR #1208: Introduce appdir before it's used; simplify description of what a view is (James E Keenan) * GH #1218: By request, remove David Golden from list of core developers. Created "emeritus" section to honor the contributions of former core developers. Thanks, xdg! 0.200003 2016-07-11 17:17:57+02:00 Europe/Amsterdam [ BUG FIXES ] * PR #1198: Session::YAML should not accept bad session cookie value from client (Peter Mottram) * Require minimum version of YAML of 0.86 (to satisfy GH #899) and a maximum version of YAML 1.15. YAML 1.16 causes test failures as reported by CPAN Testers. * Remove session test data from builds. (Peter Mottram) [ ENHANCEMENTS ] * Require minimum version of ExtUtils::MakeMaker of 7.1101 to support a range of prereq version numbers (rjbs, Jason Crome, Sawyer X) * GH #1188: Add error message to open_file (exercism-1) * Support showing private variables in templates under Template::Toolkit. (Alberto Simões) [ DOCUMENTATION ] * GH #1193: Spelling correction (Gregor Herrmann) * Fix typo of config option in Pod. (Nuno Carvalho) * Fix POD syntax error. (Nuno Carvalho) * Fix Manual error. (James E Keenan) * Move documentation index to dancer2. (Alan Berndt) * GH #1209: Clean up examples for 'set views' and 'set public_dir' in Dancer2::Manual (James E Keenan) 0.200002 2016-06-22 16:39:13+02:00 Europe/Amsterdam [ BUG FIXES ] * Using `var` with a `forward`ed request now works. (Sawyer X, Jason Crome) 0.200001 2016-06-16 15:51:04+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #1175: Plugins are not required to be in the Dancer2::Plugin namespace. (Russell @veryrusty Jenkins) * GH #1176, #1177: Remove Test::Deep as a test dependency. (Nuno Carvalho, Peter Mottram) * GH #1185: Fails on 5.25.1. (Tony Cook) [ DOCUMENTATION ] * GH #1178: Update D2::Manual with links to new plugin architecture. (Joel Berger, Jason A. Crome) * GH #1184: Use 'before_template_render' rather than the special case 'before_template' in D2::Manual and D2::Tutorial (Philippe Bricout) [ ENHANCEMENTS ] * GH #1018: Additional plugin hook tests (Ruben Amortegui) 0.200000 2016-05-31 15:05:46+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #1174: Update plugin tests to stop deprecation warnings (Peter Mottram) * GH #1173: Reword error when serialization / deserialization fails to be more generic (Russell @veryrusty Jenkins) [ ENHANCEMENTS ] * Introduce an improved variation of the Dancer2::Plugin::SendAs into core. You can now override the serializer (or lack thereof) at any point in time for a response by calling `send_as`. You can also send the options of `send_file` (like the Content-Type) and the charset for the app is also respected. (Russell @veryrusty Jenkins) 0.166001_04 2016-05-27 14:54:53+02:00 Europe/Amsterdam (TRIAL RELEASE) [ BUG FIXES ] * GH #1171: Ensure request query parameter parsing is independent of Plack version (Russell Jenkins) 0.166001_03 2016-05-27 13:23:52+02:00 Europe/Amsterdam (TRIAL RELEASE) [ BUG FIXES ] * GH #1165, #1167: Copy is_behind_proxy attribute into new request on forward. (Russell Jenkins) [ ENHANCEMENTS ] * GH #1120: Move from MooX::Types::MooseLike to Type::Tiny for performance. (Peter Mottram) * GH #1145, #1164: Replace Class::Load with Module::Runtime (Sawyer X) * GH #1159, #1163: Make template keyword global. (Sawyer X, Russell Jenkins) [ DOCUMENTATION ] * GH #1158: List both static and shared modules in Apache's deploy instructions. (Varadinsky) 0.166001_02 2016-04-29 16:42:54+02:00 Europe/Amsterdam (TRIAL RELEASE) [ BUG FIXES ] * GH #1160: Engines receive correct log callback on build (Peter Mottram) * GH #1148: Ensure request body parameter parsing is independent of Plack version (Russell Jenkins) 0.166001_01 2016-04-19 21:50:35+02:00 Europe/Amsterdam (TRIAL RELEASE) [ BUG FIXES ] * GH #1102: Handle multiple '..' in file path utilities. (Oleg A. Mamontov, Peter Mottram) * GH #1114: Fix missing prereqs as reported by CPANTS. (Mohammad S Anwar) * GH #1128: Shh warning if optional megasplat is not present. (David Precious) * GH #1139: Fix incorrect Content-Length header added by AutoPage handler (Michael Kröll, Russell Jenkins) * GH #1144: Change tt tags to span in skel (Jason Lewis) * GH #1046: "no_server_tokens" configuration option doesn't work. (Sawyer X) # GH #1155, #1157: Fix megasplat value splitting when there are empty trailing path segments. (Tatsuhiko Miyagawa, Russell Jenkins) NOTE: Paths matching a megasplat that end with a '/' will now include an empty string as the last value. For the route pattern '/foo/**', the path '/foo/bar', the megasplat gives ['bar'], whereas '/foo/bar/' now gives ['bar','']. Joining the array of megasplat values will now always be the string matched against for the megasplit. [ DOCUMENTATION ] * GH #1119: Improve the deployment documentation. (Andrew Beverley) * GH #1123: Document import of utf8 pragma. (Victor Adam) * GH #1132: Fix spelling mistakes in POD (Gregor Herrmann) * GH #1134: Fix spelling errors detected by codespell (James McCoy) * GH #1153: Fix POD rendering error. (Sawyer X) [ ENHANCEMENTS ] * GH #1129: engine.logger.* hooks are called around logging a message. (Russell @veryrusty Jenkins) * GH #1146: Cleaner display of error context (Vernon Lyon) * GH #1085: Add consistent keywords for accessing headers; 'request_header' for request, 'response_header', 'response_headers' and 'push_response_header' for response. (Russell @veryrusty Jenkins) * GH #1010: New Dancer2::Plugin architecture, includes support for plugins using other plugins. (Yanick Champoux, Russell Jenkins, Sawyer X, Damien Krotkine, Stefan @racke Hornburg, Peter Mottram) Note: Considerable effort has gone into working with the authors of existing plugins to ensure their plugins are compatible with both the 'old' and the new reworked plugin architecture. Please upgrade your plugins to a recent release. (Special thanks to Peter @SysPete Mottram) 0.166001 2016-01-22 07:54:46+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #1105, #1106, #1108: Autopage + Template Toolkit broke in last release. (Kaitlyn Parkhurst @symkat, Russell Jenkins) 0.166000 2016-01-12 19:01:51+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #1013, #1092: Remove race condition caused by caching available engines. (Sawyer X, Menno Blom, Russell Jenkins) * GH #1089: Exact macthing of route regex comments for tokens/splats. (Sawyer X) * GH #1079, #1082: Allow routes to return '0' as response content, and serializer hooks are called when default response content is to be returned. (Alberto Simões, Russell Jenkins) * GH #1093, 1095: Use a dynamic TT2 INCLUDE_PATH to allow relative views with relative includes; fixing regression introduced by #1037. (Russell Jenkins) * GH #1096, #1097: Return compatibility on Perl 5.8.x! (Peter Mottram - @SysPete) [ DOCUMENTATION ] * GH #1076: Typo in Dancer2::Core::Hook POD. (Jonathan Scott Duff) [ ENHANCEMENTS ] * GH #1074: Add sample session engine config to skeleton app. (Peter Mottram - @SysPete) * GH #1088: Return route objects when defining new routes. (Sawyer X) 0.165000 2015-12-17 09:19:13+01:00 Europe/Amsterdam [ BUG FIXES ] * Revert session_name change, as this would invalidate all existing changes. We will need to rethink this change. (Stefan @racke Hornburg, Sawyer X) 0.164000 2015-12-16 23:42:24+01:00 Europe/Amsterdam [ DOCUMENTATION ] * Update core team members and contributors list. (Russell Jenkins) * GH #1066: Fix typo in Cookbook. (gertvanoss) * Correct typo. It's "query_parameters", not "request_parameters". Thanks to mst for letting me know and making sure I fix it! (Sawyer X) [ BUG FIXES ] * GH #1040: Forward with a post body no longer tries to re-read body filehandle. (Bas Bloemsaat) * GH #1042: Add Diggest::SHA as explicit prequisite for installs on perl < v5.9.3. (Russell Jenkins) * GH #1071, #1070: HTML escape the message in the default error page. (Peter Mottram) * GH #1062, #1063: Command line interface didn't support "-s SKELETON_DIRECTORY" in any order. (Nuno Carvalho) * GH #1052, #1053: Always call before_serializer hook when serializer is set. (Mickey Nasriachi) * GH #1034: Correctly use different session cookie name for Dancer2. (Jason A. Crome) * GH #1060: Remove trailing slashes when providing skeleton directory. (Gabor Szabo) [ ENHANCEMENTS ] * Use Plack 1.0035 to make sure you only have HTTP::Headers::Fast in the Plack::Request object internally. * GH #951 #1037: Dancer2::Template::TemplateToolkit no longer sets TT2 INCLUDE_PATH directive, allowing `views` setting to be non-absolute paths. (Russell Jenkins) * GH #1032 #1043: Add .dancer file to new app scaffolding. (Jason A. Crome) * GH #1045: Small cleanups to Request class. (Russell Jenkins) * GH #1033: strict && warnings in Dancer2::CLI. (Mohammad S Anwar) * GH #1052, #1053: Allow before_serializer hook to change the content using @_. (Mickey Nasriachi) * GH #1060: Ignore .git directory when using an external skeleton directory. (Gabor Szabo) * GH #1060: Support more asset file extensions. (Gabor Szabo) * GH #1072: Add request->is_options(). (Theo van Hoesel) 0.163000 2015-10-15 12:47:57+02:00 Europe/Amsterdam [ DOCUMENTATION ] * GH: #1030: Fix pod references pointing to Dancer package (Mohammad S Anwar, Russell Jenkins) 0.162000_01 2015-10-13 17:05:09+02:00 Europe/Amsterdam (TRIAL RELEASE) [ BUG FIXES ] * GH #996: Fix warning with optional arguments. (Bas Bloemsaat) * GH #1001: Do not trigger an internal error on 404. (Russell Jenkins) * GH #1008,#976: Hack to quiet warning while plugins architecture is being rewritten. (Russell Jenkins) * Use Safe::Isa when calling their functions in the respected eval. (Sawyer X) [ ENHANCEMENTS ] * GH #738, #740, #988: route_parameters, query_parameters, and body_parameters keywords added, providing Hash::MultiValue objects! (Sawyer X) * #941, #999: delayed() keyword now has "on_error" option for controlling errors. (Sawyer X) * dancer2 app now support -s switch to supply an app skeleton (Nuno Carvalho) * "perl_version" token in templates now uses $^V, not $]. (Sawyer X) * GH #966: Remove Dist::Zilla::Plugin::AutoPrereqs. (Vernon) * GH #992: Deprecate creating route named placeholders ":captures" and ":splat". (Sawyer X) * Bump Moo requirement to 2.000000. (Alberto Simões) * GH #1012: Add :nopragmas import flag. (Sawyer X) [ DOCUMENTATION ] * GH #974: Use correct classname. (Sawyer X) * GH #958: Fix manual example with loading additional routes. (Sawyer X) * GH #960: Fix a few links. (Sawyer X) * Document you can install Scope::Upper for greater speed. (Sawyer X) * GH #1000: Correct POD name for Dancer2::Manual::Deployment. (Jason A. Crome) * GH #1017: Fix instructions on running app.psgi. Highlight beginner-friendly application running instructions. (Jason Crome) * GH #920, #1020: Remove deprecated functionality from example plugin. (Jason Crome) * GH #1002: Correct execute_hook() call in plugins documentation. (Jason Crome) * Expand on auto-reloading options using Plack Shotgun loader. (Jason Crome, @girlwithglasses) * GH #1024: Document the need to define static_handler when changing the public_dir option. (Sébastien Deseille) 0.162000 2015-09-06 13:08:05+02:00 Europe/Amsterdam [ BUG FIXES ] * Not exactly bug fix, but now captures() always returns hashref. (Sawyer X) * GH #931: Using params() keyword, route parameters now override body parameters which override query parameters. (Sawyer X) [ ENHANCEMENTS ] * Small speed bump: use eval{} instead of Try::Tiny. (Sawyer X) [ DOCUMENTATION ] * Replace File::Slurp with File::Slurper in tutorial. (Nick Tonkin) 0.161000_01 2015-08-28 15:29:00+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #947, #948: Escape file paths in regex patterns. (A. Sinan Unur) * GH #944: Setting response content in before hook when a serializer is set no longer triggers an error. (Russell Jenkins, Dmitrii Tcyganov) * GH #965: Remove non-existant role from Response::Delayed. (Vernon, Russell Jenkins) * GH #971: Route options matching no longer uses each iterator. (Tina Müller) * GH #959: Custom error template rendering fixed. (Russell Jenkins) * GH #961: Render custom error templates in before hooks. (Russell Jenkins) * GH #978: Tests - fix response regex after html_encode (Vernon) * GH #972: Exceptions thrown by serializers no longer masked. (Russell Jenkins) [ DOCUMENTATION ] * GH #967: Fix upload example. (Alberto Simões) * GH #881: Add cookie timeout example. (Andy Beverley) * GH #963: Document all available template tokens. (Sawyer X) [ ENHANCEMENTS ] * Optimize the s*#t out of basic routing. Faster than Dancer 1 now. (Sawyer X) * Only load HTTP::Server::PSGI when asked to start a development server not under Plack. (Sawyer X, Mickey Nasriachi) * GH #949: Produce cleaner, non-verbose test output (Vernon) * GH #950: Decode characters in param keys (Patrick Zimmermann) * GH #914: Include stack trace on default error page when show_errors is true. (Vernon) * GH #980, #981: halt keyword sets response content if provided, as Dancer 1 does. (Achilles Kars) * GH #909, #957, #983: HTML5 templates in generated apps and default error template (Gabor Szabo, Kadir, Vernon) * GH #972, #719, #969, #644, #647: Streamline serializer helpers. to_json/from_json now faster. (Russell Jenkins) 0.161000 2015-07-08 14:57:16+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #915, #930: Check existence of optional extension headers when behind proxy. (Andy Beverley, Pedro Melo, Russell Jenkins) * GH #926, #940: Set session directory default to $apprdir/session. (Russell Jenkins) * GH #936, #939: Use the error_template configuration on a 404. (Russell Jenkins) * GH #844, #937: Non-hash serialized params do not cause a crash. (Sawyer X) * GH #943: Pass @_ to UNIVERSAL's VERSION so it validates version number. (Sawyer X) * GH #934: Cleanup internals in the old Dispatcher. (Russell Jenkins) [ DOCUMENTATION ] * Sanitize Changes * GH #938: Fix POD link to params keyword. (Ludovic Tolhurst-Cleaver) * GH #935: Provide more details and considerations when using behind_proxy. (Andy Beverley) [ ENHANCEMENT ] * GH #933: use note in tests to produce cleaner non-verbose output (Vernon) * Remove unnecessary dependencies: build chain should be smaller. (Sawyer X) * No need for Module::Build. (Sawyer X) * GH #911: Dancer2 request object is now a subclass of Plack::Request. It's also much faster now. (Sawyer X) 0.160003 2015-06-06 11:09:00+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #921, #922: Plack >= 1.0035. (Russell Jenkins, Alberto Simões) [ ENHANCEMENT ] * #922: Use HTTP::Headers::Fast in request and response objects (Russell Jenkins) 0.160002 2015-06-04 13:03:38+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #920: Sanitize session IDs in file-based sessions. (Russell Jenkins, Andrew Beverley) [ DOCUMENTATION ] * GH #908: Cleanup Dancer references in DBIC section of cookbook (Julien Fiegehenn) * GH #910: Misc spelling and grammar fixes (Gregor Herrmann) * GH #916: Fix test example. (Peter Mottram - @SysPete) * GH #912, #913: Fix documentation on when stacks are printed. (Andrew Solomon) 0.160001 2015-05-14 20:40:10+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #893, #895: Catch config parse errors when Config::Any doesn't throw them. (Russell Jenkins) * GH #899: Minimum YAML version supported is v0.86 (Shlomi Fish) * GH #906: send_file - missing import and fix logic error for streaming by default (Russell Jenkins) [ DOCUMENTATION ] * GH #897: Remove docs for unimplemented 'load' keyword (Fayland Lam) [ ENHANCEMENT ] * GH #894, #898: Add status and headers methods to ::Response::Delayed (Yanick Champoux, Charlie Gonzalez) 0.160000 2015-04-27 00:12:55+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #868: Fix incorrect access name in $error->throw. (cdmalon) * GH #879, #883: Fix version numbering in packaging and tests. (Russell Jenkins) * File serving (send_file) won't call serializer. (Russell Jenkins) * GH #892, #510: Workaround for multiple plugins with hooks. (Russell Jenkins, Alberto Simões) * GH #558: Remove "prefix" inconsistency with possibly missing postfixed forward slash. (Sawyer X) [ DOCUMENTATION ] * GH #816, #874 Document session engine changes in migration documentation. (Chenchen Zhao) * GH #866, #870: Clarify that you cannot forward to a static file, why, and two different ways of accomplishing it without forward. (Sakshee Vijayvargia) * GH #878: Rework example for optional named matching due to operator precedence. (Andrew Solomon) * GH #844: Document Simple session backend is the default. (Sawyer X) [ ENHANCEMENT ] * GH #869: Streaming file serving (send_file). (Russell Jenkins) * GH #793: "prefix" now supports the path definition spec. (Sawyer X) * GH #817, #845: Route spec under a prefix doesn't need to start with a slash (but must without a prefix). (Sawyer X, Russell Jenkins) * GH #871: Use Safe.pm instead of eval with Dancer2::Serializer::Dumper. (David Zurborg) * GH #880: Reduce and cleanup different logging calls in order to handle the stack frames traceback for logging classes. (Russell Jenkins) * GH #857, #875: When failing to render in Template::Toolkit, make the error reflect it's a TT error, not an internal one. (valerycodes) 0.159003 2015-03-23 14:57:15+01:00 Europe/Amsterdam [ BUG FIXES ] * Fixed another memory leak with compiled hooks. (Sawyer X) * Fixed a memory leak with conditionally applied static middleware (Russell Jenkins) [ DOCUMENTATION ] * GH #854, #858: Fix after_template_render hook example. (Adam Weinberger) * GH #861: Improve documentation of 'forward'. (Andy Beverley) 0.159002 2015-03-03 19:21:21+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #856: Memory leak when throwing exception from a hook. (Sawyer X) 0.159001 2015-02-25 15:31:35+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #855: Ensure Dancer2::Test is compatible with Pod::Simple 3.30. (Russell Jenkins) [ DOCUMENTATION ] * Add an example for delayed (async) streaming response. (Sawyer X) * Small link fix. (Sawyer X) 0.159000 2015-02-24 04:51:20+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #762: Delay app cleanup until errors are rendered. (Russell Jenkins) * GH #835: Correct Logic error in Logger if no request exists. (Lennart Hengstmengel) * GH #839: Correct "no_server_tokens" definition in production.yml. (Nikita K) * GH #853, #852: Handle malformed (contentless) cookies. (pants) * GH #840, #842: Ensure session data available to template engines. (Russell Jenkins) * GH #565, #847, #849: Fix HTTP Status template logic and documentation. (Daniel Muey, Russell Jenkins, Dávid Kovács) * GH #843: Add missing attributes to Moo class used in tests. (Graham Knop) [ ENHANCEMENT ] * GH #836: Support delayed (asynchronous) responses! ("Delayed responses" in Dancer2::Manual for more information.) (Sawyer X) * GH #824: Use Plack::MIME by default, MIME::Types as failback if available. (Alberto Simões) * GH #792, #848: Keywords can now use prototypes. (Russell Jenkins, Sawyer X) [ DOCUMENTATION ] * GH #837, #838, #841: Major documentation restructure. (Snigdha Dagar) (Check eb9416e9 and a78e27d7 for more details.) * GH #823: Cleanup Manual and Cookbook docs. (Omar M. Othman) * GH #828: Provide README.mkdn. (Nuno Carvalho) * GH #830: Fix typo in Session::YAML pod. (Vince W) * GH #831,#832: Fix broken link in Session::YAML pod. (Vince W) 0.158000 2015-01-01 18:08:04+01:00 Europe/Amsterdam ** Happy new year! ** [ ENHANCEMENT ] * GH #778: Avoid hard-coded static page location. (Dávid Kovács) * Speed up big uploads significantly. (Rick Myers) * GH #821: Use Import::Into to import pragmas. (Russell Jenkins) * GH #782: Fix utf8 pragma import. (Maxim Vuets) * GH #786: Perlbrew fix. (Dávid Kovács) * GH #622: Refactoring. (James Raspass) [ DOCUMENTATION ] * GH #713: Change order of statements in Cookbook to not imply that D2::P::Ajax::ajax() calls have priority. (Sawyer X) 0.157001 2014-12-21 20:40:13+01:00 Europe/Amsterdam [ ENHANCEMENT ] * GH #814, #815: Rename "app.pl" to "app.psgi". (Sawyer X) 0.157000 2014-12-14 18:23:33+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #799: Set current request earlier so log formats using requests will work. (Sawyer X) * GH #650: Provide default environment to app for templating. (Dávid Kovács, Chi Trinh) * GH #800: Better portability code, for different Windows situations. (Christian Walde) * Less littering of the test directories with session files. (Sawyer X) [ ENHANCEMENT ] * GH #810: strict && warnings in the app.pl. (Sawyer X) * Use to_app keyword in skeleton. (Sawyer X) * GH #801: Under production, server tokens are disabled. (Sawyer X) * GH #588, #779: Remove LWP::UserAgent in favor of HTTP::Tiny. (Dávid Kovács, simbabque, Sawyer X) * Remove all usages of Test::TCP in favor of Plack::Test. (Sawyer X) [ DOCUMENTATION ] * GH #802: Remove indication of warnings configuration option and add explanation in migration document. (Sawyer X) * GH #806: Link in main docs to the migration document. (Gabor Szabo) * GH #807: Update migration document with more session data, changes to app.pl, and Template::Toolkit configuration. (Gabor Szabo) * GH #813: Update migration document with information on encoding and usage of Plack::Request internally. (Gabor Szabo, Sawyer X) 0.156001 2014-12-08 23:03:43+01:00 Europe/Amsterdam [ DOCUMENTATION ] * Documentations suggested serializers aren't consistent. We fixed it so we make sure docs reflect that. (Sawyer X) 0.156000 2014-12-07 18:04:14+01:00 Europe/Amsterdam [ BUG FIXES ] * Do not try to deserialize empty content. (Lennart Hengstmengel, Sawyer X) * Do not call serialization hooks when no serialization took place. (Sawyer X) * Be more cautious on undef output from serializer. (Daniel Böhmer, Sawyer X) [ ENHANCEMENTS ] * Add cpanfile when scaffolding a new app. (Dávid Kovács, Sawyer X) * Response "content" attribute no longer stringifies. This should help reduce warnings, odd debugging problems, etc. (Sawyer X) * DSL "uri_for" no longer returns URI object. Instead just the URI. (Sawyer X) [ DOCUMENTATION ] * GH #777: Fix doc for mentioning public dir. (Dávid Kovács, Sawyer X) * GH #787: Document all environment variables. (Sawyer X) 0.155004 2014-12-04 11:51:23+01:00 Europe/Amsterdam [ BUG FIXES ] * Guard against content length being empty strings. This is really bizarre case but saw it once. (Sawyer X) 0.155003 2014-12-03 22:32:12+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #798: More test fixes on Windows. (A. Sinan Unur) 0.155002 2014-12-02 22:59:32+01:00 Europe/Amsterdam [ BUG FIXES ] * Fix test on Windows. (A. Sinan Unur) 0.155001 2014-11-28 17:42:24+01:00 Europe/Amsterdam [ BUG FIXES ] * Small typo in test. (Dávid Kovács) 0.155000 2014-11-28 01:18:39+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #773, #775: AutoPage handler no longer renders layouts. (Dávid Kovács, Sawyer X) * GH #770: Prevent crazy race condition between the logger engine and other engines. This means engines now call "log_cb" to log. (Sawyer X) * App now has default name to caller package. (Sawyer X) * Serializers will not try to serialize empty content. (Sawyer X) * Lots of cleanups in Core::Request in favor of Plack::Request. (Sawyer X) [ ENHANCEMENTS ] * Layouts directory can be configured using 'layout_dir'. (Sawyer X) * GH #648, #760: Logger format now supports 'h', 'u', 'U', 'h', 'i'. They are documented but weren't really available. (Lennart Hengstmengel) * Serializers having errors will not fail if there is no logger. (Sawyer X) * Create a request object with a single argument of $env, like Plack::Request. (Sawyer X) [ DOCUMENTATION ] * Remove documented hack for static content because we use the middleware now anyway. (Sawyer X) * Document further the difference between splat and megasplat. (Dávid Kovács) 0.154000 2014-11-17 15:36:31+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #744: Serialize anything, not just references. (Sawyer X) * GH #744: Serialize regardless of content_type of serializer. (Sawyer X) * GH #764: Catch template render errors. (Russell Jenkins, Steven Humphrey) * Calling uri_for(undef) doesn't crash. (Sawyer X) * GH #732: Correct name for 403 (Forbidden, not Unauthorized). (Theo van Hoesel, Sawyer X, Mickey Nasriachi, Omar M. Othman) * GH #753: Syntax of parameterized types. (Russell Jenkins) * GH #734: Failing tests on Windows. (Russell Jenkins, Sawyer X) [ ENHANCEMENTS ] * GH #664, #684, #715: Handler::File replaced for static files with Plack::Middleware::Static, allowing files to be served *before* routes. This means hooks do not apply to static files anymore! (Russell Jenkins, DavsX) * Engines now have "logger" attribute to log errors. It sends the Dancer2::Logger:: object, if one exists. (Sawyer X) * Serializers do not need to implement "loaded" method. (Sawyer X) * GH #733: In core: response_xxx removed in favor of generic standard_response. (Sawyer X, Mickey Nasriachi, Omar M. Othman) * GH #514, #642, #729: Allow mixing named params, splat, and megasplat. (Russell Jenkins, Johan Spade, Dávid Kovács) * GH #596: no_server_tokens works, as well as DANCER_NO_SERVER_TOKENS. (Omar M. Othman, Sawyer X, Mickey Nasriachi) * GH #639: Validate engine types in configuration. (Sawyer X, Omar M. Othman, Mickey Nasriachi, Russell Jenkins) * GH #663, #741: Remove "accept_type" attribute and other references. (Mickey Nasriachi, Theo van Hoesel) * GH #748: Provide forwarded_host, forwarded_protocol. (Sawyer X) * GH #748: Do not provide a default env, require env for a request. (Sawyer X) * GH #742: Update test skeleton to use to_app. (Dávid Kovács) * GH #636: Use Plack::Test in more tests. (Dávid Kovács) [ DOCUMENTATION ] * GH #656: Dancer2::Manual::Testing as a guide for testing Dancer2 applications. (Sawyer X) * Improved documentation of route matching. (Russell Jenkins) * Migration document update relating to enhancements. (Sawyer X, Mickey Nasriachi) * GH #731: Document changes in session. (racke, Sawyer X, Mickey Nasriachi, Omar M. Othman) * Document "id" attribute in Request object. (Sawyer X) * Correct Cookbook examples on command line scripts. (Sawyer X) 0.153002 2014-10-30 09:23:52+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #734: More failing tests. (Sawyer X) 0.153001 2014-10-27 12:39:54+01:00 Europe/Amsterdam [ BUG FIXES ] * GH #734: Failing tests on Windows. (Sawyer X) [ DOCUMENTATION ] * GH #724: Plack::Test example in Dancer2::Test. (Jakob Voss) 0.153000 2014-10-23 23:45:36+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #634, #687: Fix file logger defaults. (Russell Jenkins, Dávid Kovács, Sawyer X) * GH #730: Make App use app-level config for behind_proxy. (Sawyer X) * GH #727: Disable regex metachars when calculating app location in tests (Gregor Herrmann) * GH #681, #682, #712: Clear session engine within destroy_session. (DavX, Russell Jenkins) * Ignore :tests in importing, don't suggest :script. (Sawyer X) [ ENHANCEMENT ] * Internal: Move the implementation of send_file from DSL to App. (Russell Jenkins) [ DOCUMENTATION ] * GH #728: Typos in Policy document. (Olaf Alders, Sawyer X) 0.152000 2014-10-14 04:30:59+02:00 Europe/Amsterdam [ BUG FIXES ] * GH #723: Redispatched requests lose data. (Sawyer X) [ ENHANCEMENT ] * Provide 'content' keyword to set the response content. (Sawyer x) * GH #616, #155, #615: Session engines are now lazy! (Russell Jenkins) * Dancer2 response objects can be created from arrays or from Plack::Response objects. (Sawyer X) * GH #718: Clean up App's Template engine. (Russell Jenkins) * Adding class-based tests. (Sawyer X) [ DOCUMENTATION ] * Add a policy document under Dancer2::Policy. (Sawyer X) * Document log_format instead of logger_format. (Sawyer X) 0.151000 2014-10-08 21:49:06+02:00 Europe/Amsterdam [ ENHANCEMENT ] * Apps are now a proper independent PSGI application. Forwarding and passing requests between apps will still work if you use the 'Dancer2->psgi_app' method without providing a class, but it might still be phased out in the future. (Sawyer X) [ DOCUMENTATION ] * Migration document! (Snigdha Dagar) * GH #667: Fix typo in cookbook pod. (Lindsey Beesley) * GH #649, #670: Document core logger. (simbabque) * GH #689: Git guide markdown fixes. (Paul Cochrane) * GH #690, #691, #694, #696, #698, #699, #700, #702, #703, #704, #705, #706, #707, #708, #710: Doc cleanups. (Paul Cochrane) * GH #688: Improve testing documentation. (Paul Chochrane) * GH #692: Document serving static files using Plack::Middleware::Static. (Dávid Kovács @DavsX) * GH #695: Correct Dancer2::Logger::Capture, add test example. (Dávid Kovács @DavsX) * GH #716: Correct document on proxy procotol forwarding in Apache. (Andy Beverley) 0.150000 2014-08-17 01:35:16CEST+0200 Europe/Amsterdam [ DOCUMENTATION ] * GH #657: Update multi-app example in cookbook to include route merging. (Bas Bloemsaat) * GH #643: Improve session factory docs by mentioning Dancer2::Config. (Andy Jack) [ BUG FIXES ] * Postponed hooks are no longer sent to all Apps. (Sawyer X, Mickey Nasriachi) * 404 File Not Found Application reworked to stay up to date with postponed hooks merging in multiple apps. (Russell Jenkins) * GH #610, #662: Removed two circular references memory leaks! (Russell Jenkins) * GH #633: Log an error when a hook dies. (DavsX) [ ENHANCEMENT ] * Allow settings apps in the psgi_app() call by name or regex. (Sawyer X) * GH #651: silly typo in clearer method name (DavsX). 0.149000_02 2014-08-10 13:50:39CEST+0200 Europe/Amsterdam [ ENHANCEMENT ] * GH #641: Adding a shim layer to prevent available hooks (and thus plugins) from breaking. * Each App can now define its own configuration. The Runner's application-specific configure has been untangled. (Russell @veryrusty Jenkins, Sawyer X, Mickey Nasriachi) * Multiple Dancer App support. You can now create a App-specific PSGI application using MyApp->psgi_app. (Russell @veryrusty Jenkins, Sawyer X, Mickey Nasriachi) * Add routes and hooks to an existing app on import. (Russell @veryrusty Jenkins, Stevan Humphrey, Stefan racke Hornburg, Jean Stebens, Chunzi, Sawyer X, Mickey Nasriachi) * Allow DSL class to be specified in configuration file. (Stevan Humphrey) * forward() now returns a new request which is then just runs the dispatching loop again. (Sawyer X, Mickey Nasriachi) [ BUG FIXES ] * GH #336: Set log level correctly. (Andrew Solomon, Pedro Bruno) * GH #627, #607: Remove potential context issues with returning undef explicitly. (Javier Rojas) * GH #646: Fix whitespacing for tests. (DavsX) 0.149000_01 2014-07-23 21:31:21CEST+0200 Europe/Amsterdam *************************** NOTICE *************************** * This very is a major upgrade * * We untangled the context, DSL implementation a bit * * Please check your code, including your plugins, thoroughly * * Thank you * [ ENHANCEMENTS ] * GH #589: Removing Dancer2::Core::Context global context variable. Finally in. (Sawyer X, Mickey Nasriachi, Russell @veryrusty Jenkins) [ BUG FIXES ] * GH #606, #605: Fix for setting public directory. (Ivan Kocienski, Russell Jenkins, Stefan @racke Hornburg) * GH #618, #620: Fix jQuery link generated by CLI skeleton. (Michał Wojciechowski) * GH #589: Major memory leak fix by removal of Dancer2::Core::Context. [ ENHANCEMENTS ] * GH #620: Bump jQuery to 1.11.1. (Michał Wojciechowski) 0.143000 2014-07-05 21:39:28CEST+0200 Europe/Amsterdam [ BUG FIXES ] * GH #538, #539: Coerce propogated exceptions to strings within Error object. (Steven Humphrey) * GH #531: Generate valid HTML when show_errors is true from Error objects. (Steven Humphrey) * GH #603: Update skeleton test to use Plack::Test. (Sawyer X) [ ENHANCEMENTS ] * Provide psgi_app in top-level Dancer.pm to make it easier to change it. (Sawyer X) 0.142000 2014-06-24 15:16:42CEST+0200 Europe/Amsterdam [ BUG FIXES ] * GH #550, #555: Allow the content type to be set when using send_file as per the documentation. (Russell Jenkins, Steven Humphrey) [ ENHANCEMENTS ] * GH #512, #520, #602: Pass all settings into JSON serializer engine. (Jakob Voss, Russell Jenkins) * GH #532: Serialize runtime errors such as those produced by die if a serializer exists. (Steven Humphrey) 0.141000 2014-06-08 22:27:03CEST+0200 Europe/Amsterdam * No functional changes. 0.140900_01 2014-06-07 23:32:56IDT+0300 Asia/Jerusalem [ BUG FIXES ] * GH #447: Setting the apphandler now triggers the Dancer Runner configuration change, which works. (Sawyer X) * GH #578: Remove the default engine configurations. (Sawyer X) * GH #567: Check for proper module names in loading engines. Might help with taint mode. (Sawyer X) * GH #585, #595: Return 405 Method Not Allowed instead of 500. (Omar M. Othman) * GH #570, #579: Ensure keywords pass, send_error and send_file exit immediately when executed. (Russell Jenkins) [ ENHANCEMENTS ] * GH #587: Serializer::Mutable alive! (Pedro Bruno) [ DOCUMENTATION ] * Fix doc for params(). Ported from Dancer#1025 (Stefan Hornburg) 0.140001 2014-05-01 10:49:25CEST+0200 Europe/Amsterdam [ BUG FIXES ] * Bugfix for extracting multiple cookies within a request. (Cymon, Russell Jenkins) * Require minimum version of Plack to make sure we can add the Head middleware. Not exactly a bug, but not a feature. (Sawyer X) [ DOCUMENTATION ] * Correct reference to HTTP::Server::Simple::PSGI. (Russell Jenkins) 0.140000 2014-04-28 23:14:31CEST+0200 Europe/Amsterdam [ ENHANCEMENTS ] * Replace Config role with better ConfigReader role. (Mickey Nasriachi, Stefan Hornburg, Sawyer X) * Move App-related attributes (engines) to App instead of config role. (Mickey Nasriachi, Stefan Hornburg, Sawyer X) * Untangle Runner-Server (removing Server entirely). (Mickey Nasriachi, Stefan Hornburg, Sawyer X) * Replace HTTP::Server::Simple::PSGI with HTTP::Server::PSGI. (Mickey Nasriachi, Stefan Hornburg, Sawyer X) * GH #527: Build request cookie objects from request headers, not env. (Russell Jenkins) * GH #569: Transform cookie using the HTTP_COOKIE header, per PSGI spec. (Russell Jenkins) * GH #559, #544: Use Plack middleware for HEAD request content removal. (Russell Jenkins) * GH #513, #483: Deserialize body content for DELETE requests. (Russell Jenkins, Yanick Champoux, Sawyer X) 0.13 2014-04-13 19:19:44CEST+0200 Europe/Amsterdam [ ENHANCEMENTS ] * GH #562: Change YAML::Any to YAML (Steven Humphrey, Russell Jenkins). [ BUG FIXES ] * GH #524: Double encoding for YAML sessions. * GH #557: Switch to using YAML::Old. * GH #548: Deserializer test failure. 0.12 2014-04-07 22:42:12 Europe/Amsterdam [ ENHANCEMENTS ] * GH#518: Bump jQuery to 1.10.2 (Grzegorz Rożniecki). * GH#535: Support OPTIONS and PATCH requests in Server::Standalone. (Russell Jenkins) * GH#553: Dancer2 CLI: specify directory to write app skeleton (Jean Stebens) * GH#543: Additional HTTP Methods for Ajax plugin (Jean Stebens). [ DOCUMENTATION ] * RT#91428: POD encoding set to UTF-8 in main .pm (Gregor Herrmann). * GH#517: Miscellaneous documentation fixes (Cesare Gargano). * GH#518: "Getting started" demo page fixes (Grzegorz Rożniecki). * GH#522: s/PerlHandler/PerlResponseHandler/ in Apache2 sample configuration (Grzegorz Rożniecki) * GH#521: Remove duplicated POD and clean up list details (Shlomi Fish) * GH#526: Cleanup POD formating and code snippets in manual. (Grzegorz Rożniecki) [ BUG FIXES ] * GH#528,529: Force PSGI server in dispatch scripts for CGI or fcgi deployments (Erik Smit, Alberto Simões) * GH#550,GH#551: Update all headers in Handler::File (Sawyer X, Stefan @racke Hornburg) * GH#540: Fix hook execution when default scalar was used in hook code. (baynes, Russell Jenkins) * GH#552: Rework test suite to use Plack::Test (Sawyer X, Stefan @racke Hornburg) * GH#560: Return value of hooks do not alter response content. (Jean Stebens) 0.11 2013-12-15 14:19:22 Europe/Amsterdam [ ENHANCEMENTS ] * GH#481: Don't pollute @INC automatically when Dancer2 is imported, each runner is now responsible of including the local ./lib dir if needed. * GH#469, 418: Dancer2::Plugin provides a ':no_dsl' flag for modern Plugins (Pedro Melo) * GH#485: Keywords 'redirect' and 'forward' exit immediately when executed in a route/hook. New dependency on Return::MultiLevel (Russell Jenkins). * GH#495: Use accessor and predicates instead of direct access. Addresses GH#493 too. (Russell Jenkins) * GH#502,GH#472: Rework halt to use with_return from Return::MultiLevel. (Russell Jenkins) * GH#479,GH#480,GH#508: Pass parameters to params() in the DSL. (Slava Goltser, unickuity, Russell Jenkins) * GH#505: Fix empty HTTP_REFERER in Dancer::Core::Request (Menno Blom). * GH#503: Multiple reverse proxy support (Menno Blom). * GH#371,GH#506: CLI tool rewrite (using App::Cmd, supports plugins, etc.). (Ivan Kruglov, Samit Badle, Sawyer X) * GH#498: Add some missing items in MANIFEST.SKIP (Gabor Szabo, Sawyer X). [ DOCUMENTATION ] * GH#489: Remove link to Dancer2::Deployment pod which does not exist (Sweet-kid) * GH#511: s/Deflator/Deflater/; (Cesare Gargano) * GH#491: Updated config paths for template_toolkit in cookbook. (Mark A. Stratman) * GH#494: Update session config details (Dancer2::Config), namespace fixup in Dancer2::Core::cookie. (Russell Jenkins) * GH#470: Fix Plack::Builder mount usage (Pedro Melo). * GH#507: Fix plenty of typos (David Steinbrunner). * GH#477: Document problem with Plack Shotgun on Windows (Ahmad M. Zawawi). * GH#504: Add link to Dancer2::Plugin::Sixpack (Menno Blom). * GH#490: Document Dancer2 should be FatPackable (Sawyer X). * GH#452: Make a complete authors section, clean it up (Pau Amma). * More fixes to main documentation (Pau Amma). 0.10 2013-09-28 15:26:41 Europe/Paris [ DOCUMENTATION ] * GH#431: Miscellaneous documentation fixes (Gideon D'souza) * Small POD corrections (Ashvini V) [ ENHANCEMENTS ] * GH#482: Show the startup banner when the worker starts by default (Alexis Sukrieh). * GH#481: Include local lib dir in @INC by defaults (Alexis Sukrieh). * GH#423: Remove ':tests' from Dancer.pm import (Alberto Simões). * GH#422: Get rid of core_debug method (Alberto Simões). * GH#421: Support Plugin::Ajax content_type (Russell Jenkins). * GH#428: Make default errors CSS path relocatable (Russell Jenkins). * GH#427, GH#443: Replace global warnings with lexical (Russell Jenkins). * GH#374: Don't create an app from app.psgi (Alberto Simões). * Cleanup Core::Request, Core::Request::Upload (Mickey Nasriachi). * GH#445: Test Template::Simple (Alexis Sukrieh, Russell Jenkins). * GH#449: Test Session hooks (Gideon D'souza) * GH#434,440: Imutable attributes (Mickey Nasriachi). * GH#435: Allow send_error to serialize error (Russell Jenkins). * Add more tests to session id rw (Pedro Melo). * Whitespace cleanup (Ivan Bessarabov). [ BUG FIXES ] * GH#424,425: Fix logger tests for different timezones, and close logfile before deleting it: Windows dixit. (Gideon D'souza, Russell Jenkins) 0.09 2013-09-02 00:12:58 Asia/Jerusalem [ ENHANCEMENTS ] * Rewite DSL keyword engine (Mickey Nasriachi) * Require minimum Role::Tiny 1.003000 (Alberto Simões) * GH#382: Move Request attributes to params, and fix serializers behavior (Russell Jenkins) * GH#406: Replace Dancer2::ModuleLoader with Class::Load (Alberto Simões, Sawyer X) * GH#329: Remove 'load_app' DSL keyword. Remove reference to 'load' as well. (Sawyer X) * GH#412: Autopages are now called properly with correct MIME. (Alberto Simões) [ DOCUMENTATION ] * GH#390: minor cookbook documentation fixes (Russell Jenkins) * GH#392: remove support to auto_reload and suggest alternative in Dancer2::Cookbook (Ahmad M. Zawawi) * GH#397,407: Miscellaneous documentation fixes (Andrew Solomon) * Documentation cleanups (Alex Beamish) [ BUG FIXES ] * When compiling route regex object with prefix, add the closing anchor (Mickey Nasriachi) * GH#386: honor log level defined in config file (Alberto Simões) * GH#396,409: Miscellaneous bug fixes (Russell Jenkins) * GH#403: Fix forward behavior (Russell Jenkins) 0.08 2013-08-18 15:22:45 Asia/Jerusalem [ ENHANCEMENTS ] * GH#352: Define content_type as a property for serializers. (Franck Cuny) * Cleanup duplicate HTTP status code between Core::Error and Core::HTTP (Russel Jenkins) * GH#363: Move core methods to Dancer2::Core (Alberto Simões) * GH#362: Serializers documentation and test cleanup. (Franck Cuny) * Refactoring of the engine method. (Franck Cuny) * Misc. code cleanup. (Russel Jenkins) * GH#280: Remove the unused ':syntax' importing tag (Sawyer X) * Display startup info only if environment is "development" (Franck Cuny) * Move postponed_hooks to server from runner (Sawyer X) * Provide easier access to global runner (Sawyer X) * Bunch of code cleanups which also includes speed boost (Sawyer X) * More immutability in the runner class and config role (Sawyer X) [ BUG FIXES ] * GH#85, GH#354: Fix autopages, especially in subdirs (Stefan Hornburg, Alberto Simões) * GH#365: Fix serializer settings (Steven Humphrey) * GH#333: callerstack for logger was too short (Alberto Simões) * GH#369: Move request deserialization from Dispatcher to Content & Request (Russell Jenkins) [ DOCUMENTATION ] * GH#192: Documentation the current usage of middlewares using Plack::Builder (Sawyer X) * GH#195, GH#197, GH#372: Multiple apps with Plack::Builder (Sawyer X) * GH#348: Documentation of Role::Logger (Franck Cuny) * GH#350: Move part of README.md to GitGuide.md (Franck Cuny) * GH#353: Documentation of Role::Serializer (Alberto Simões, Franck Cuny) * Misc. minor documentation tweak (Alberto Simões, Franck Cuny) 0.07 2013-08-04 01:14:59 Asia/Jerusalem [ ENHANCEMENTS ] * GH#344, GH#284: Now forward() calls preserve sessions (cym0n, Alberto Simões) * Separation of engines from triggers and configuration (Sawyer X, Franck Cuny) * GH#347: Remove old compatibility option 'log_path' (Franck Cuny) * GH#156, GH#250, GH#349: Remove unused module (Alberto Simões, mokko) * GH#331: Hook cleanups and documentation. (Franck Cuny) * GH#335: Serializing cleanup. (Franck Cuny) * GH#332: Clean up multiple definitions of core_debug (Franck Cuny) * GH#338: Clean up route builder (Mickey Nasriachi) * Clean up of the dzil configuration (Alberto Simões) [ BUG FIXES ] * GH#334: Fix for GH#86, to display custom 500 page/template on internal server errors (Russell Jenkins) * GH#346: Fix tests on 5.8.9 (Albert Simões) [ DOCUMENTATION ] * GH#345: Documentation reorganization (Alberto Simões, Franck Cuny) 0.06 2013-07-30 (Sawyer X) [ ENHANCEMENTS ] * Clean up of the dzil configuration (Alberto Simões,Franck Cuny, Russel Jenkins) * GH#327: Add support for 'info' log level (Russell Jenkins) * Remove 'for_versions' usage from tests (Alberto Simões) [ BUG FIXES ] * GH#326, GH#232: don't end up with empty views and layout (Franck Cuny) * GH#325: don't die or complain when two routes have the same path (Franck Cuny) * GH#320: fix plugin_setting deprecation warning (David Golden) [ DOCUMENTATION ] * POD cleanup (Sawyer X, Franck Cuny) 0.05 2013-07-20 18:51:53 Europe/Paris [ DEPRECATION ] * Dancer2::Plugin drops support for Dancer 1 (issue #207) a DEPRECATION notice is issued when a plugin uses the old syntax (Alexis Sukrieh, Mokko, David Golden) * Drop support for 'use Dancer2 :moose' (Franck Cuny) [ ENHANCEMENTS ] * Add support for HTTP_X_FORWARDED_PROTO (Yanick Champoux) * Don't inflate custom types (Graham Knop) * Encode UTF8 params in Dancer2::Test (Vincent Bachelier) * Make Dancer2::Core::Request more lazy (Franck Cuny) * Don't use rootdir for app location (David Golden) * Improve File logger (David Golden) * Drop body when status is 1x or [23]04 (Franck Cuny) * Add support for HTTP_X_FORWARDED_PROTO (Yanick Champoux) * Prevent duplicate routes from being created (Franck Cuny) * Add support for route options (Franck Cuny) * Add support for prefix with route defined with regex (Franck Cuny) * Methods to return path of views and layout in the Template role (Franck Cuny, Yanick Champoux). * GH#31, GH#221: Config merging support (Russell Jenkins) [ BUG FIXES ] * GH#272: test function 'route_doesnt_exist' was not handling test comment properly. (Jeff Boes, Yanick Champoux) * GH#228: handle UTF-8 correctly in JSON serializer (Steven Humphrey) * GH#270: handle correctly serializer's options (Keith Broughton) * GH#274: `dancer -v' returns the correct version (Dinis Rebolo) * GH#286: for HEAD request, drop response's body (Franck Cuny) * GH#293: fix defaults tests for a newly generated app (Franck Cuny) * GH#216: check 'show_errors' when returning an internal error (Franck Cuny) * GH#246: Add serialization of log messages (Stefan Hornburg) * GH#268: Dancer2::Core::Response->status accepts stringy HTTP codes (Franck Cuny) * GH#308: Add support for ENV{DANCER_CONFDIR} and ENV{DANCER_ENVDIR} (Franck Cuny) * GH#210: Don't print startup banner if startup_info is set to 0 (Maurice Mengel, Franck Cuny) * plugin_setting does not trigger a DEPRECATION warning anymore (Report by Alberto Simões, fix by Alexis Sukrieh) * GH#251: Support for on-the-fly changes of layouts/views (Franck Cuny) * GH#302: Avoid double encoding in Handler::File (Russell Jenkins) [ DOCUMENTATION ] * Lots of documentation cleanup (Mokko, David Precious) * Documenting Dancer2::Handler::AutoPage (Sabiha Imran, Sawyer X) * Documenting Dancer2::Core::Dispatcher (Babitha Balachandran) * Documenting Dancer2::Manual::DSL (David Precious, Franck Cuny) * Various typo (Shlomi Fish, Colin Kuskie, Stefan Hornburg, Rick Yakubowski) * Documenting some internals (Colin Kuskie) * Documenting Dancer2::Core::MIME (Babitha B.) * Documenting Manual::Developers (Maurice Mengel) * Documenting Dancer2::Core::Response (Colin Kuskie) 0.04 - 2013-04-22 (Alexis Sukrieh) [ BUG FIXES ] * Fix "Internal Sever Error" when sending a file with send_file (Dinis Rebolo) * Allow the setting of the 'views' directory, like stated in documentation (Alexander Karelas) [ ENHANCEMENTS ] * Implement Dancer2::Test file uploads (Steven Humphrey) * Give Dancer2::Test the ability to handle multiselect inputs (Steven Humphrey) * Make Cookie objects stringify to their value. (David Precious) * New routines for Dancer2::Test to check pod coverage in apps routes (Dinis Rebolo) * New script dancer2 to bootstrap an application (mokko) * Fix tests when running under Windows environments (Russell Jenkins) * Serializing modify the response's content type (Yanick Champoux) [ DOCUMENTATION ] * Make introduction more fluid in Dancer2's POD. (mokko) [ PACKAGING ] * Remove prereq Digest::SHA (mokko) * Dancer::P::Bcrypt recommends Dancer::P::Passphrase (Blabos de Blebe) 0.03 - 2013-03-07 (Alexis Sukrieh) [ ENHANCEMENTS ] * Don't create a session when just checking if a value exists (David Golden) * Only flush sessions if they are dirty (David Golden) * Allow the default template file extension to be changed. (David Precious) * Add on_plugin_import function to Dancer2::Plugin (David Golden) (Fix for issue #284) [BUG FIXES] * Dancer2::ModuleLoader now use Module::Runtime at its core (issue #166, Yanick Champoux) [ DOCUMENTATION ] * changed <% to [% in documentations (Alexander Karelas) * Improve Dancer2::Plugin documentation (David Golden) 0.02 - 2013-02-24 (Alexis Sukrieh) [ DOCUMENTATION ] * No more "TODO" tokens in the documentations * More documentation for Core classes (Alexis Sukrieh) [ ENHANCEMENTS ] * Removed the "api_version" code that is useless and was breaking some tests. (Alexis Sukrieh) 0.01 [ ENHANCEMENTS ] * Dancer::Test takes a hash instead of an array for better backward compatibility with Dancer 1. (Celogeek) * Session revamp: better decoupling between Session and SessionFactory, support for session destruction and session values deletion. Everythin regarding session settings is now configurable. (David Golden). * Add route_exists and route_doesnt_exist in Dancer::Test (Mokko) * session cookie duration can be expressed with human readable strings * instead of numeric values (Alexis Sukrieh, issue #157). [ BUG FIXES ] * The engine configuration is now passed down to Dancer::Template::Implementation::ForkedTiny (Damien Krotkine). * Dancer App lookup now try to detect the dir "bin" and "lib" or ".dancer" file. (Celogeek) * Issues #125 and #126 Support for configuration bits for session objects, possible to change the cookie name instead of the hard-coded value 'dancer.session'. (Reported by David Golden, fixed by Alexis Sukrieh). [ DOCUMENTATION ] * Add more POD in Dancer::Test (Mokko) 1.9999_02 * Fix tests for previous release, tests cannot assume we're under Dancer 2 when the version is 1.9999 (Alexis Sukrieh) 1.9999_01 * First DEVELEOPER release of Dancer 2 complete rewrite of Dancer with a Moo backend. (Alexis Sukrieh, David Precious, Damien Krotkine, SawyerX, Yanick Champoux and others, plus Matt S. Trout as a reviewer). AUTHORS100644001751001751 13113171470520 13735 0ustar00jasonjason000000000000Dancer2-0.205002See perldoc Dancer2.pm, section AUTHORS, for a list of core developers and contributors. t000755001751001751 013171470520 13015 5ustar00jasonjason000000000000Dancer2-0.205002app.t100644001751001751 1222113171470520 14140 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Test::Fatal; use Dancer2; use Dancer2::Core::App; use Dancer2::Core::Dispatcher; use Dancer2::Core::Hook; use Dancer2::FileUtils; use File::Spec; # our app/dispatcher object my $app = Dancer2::Core::App->new( name => 'main', ); $app->setting( show_errors => 1 ); # enable show errors my $dispatcher = Dancer2::Core::Dispatcher->new( apps => [$app] ); # first basic tests isa_ok $app, 'Dancer2::Core::App'; # some routes to play with my @routes = ( { method => 'get', regexp => '/', code => sub {'/'}, }, { method => 'get', regexp => '/blog', code => sub {'/blog'}, }, ); # testing with and without prefixes for my $p ( '/', '/mywebsite' ) { for my $r (@routes) { $app->prefix($p); $app->add_route(%$r); } } is $app->environment, 'development'; my $routes_regexps = $app->routes_regexps_for('get'); is( scalar(@$routes_regexps), 4, "route regexps are OK" ); for my $path ( '/', '/blog', '/mywebsite/', '/mywebsite/blog', ) { my $env = { REQUEST_METHOD => 'GET', PATH_INFO => $path }; my $expected = { '/' => '/', '/blog' => '/blog', '/mywebsite/' => '/', '/mywebsite/blog' => '/blog', }; my $resp = $dispatcher->dispatch($env); is $resp->[0], 200, 'got a 200'; is $resp->[2][0], $expected->{$path}, 'got expected route'; } note "testing lexical prefixes"; # clear the prefix in $app (and by the way, makes sure it works when prefix is # undef). $app->prefix(undef); # nested prefixes bitches! $app->lexical_prefix( '/foo' => sub { $app->add_route( method => 'get', regexp => '/', code => sub {'/foo/'} ); $app->add_route( method => 'get', regexp => '/second', code => sub {'/foo/second'} ); $app->lexical_prefix( '/bar' => sub { $app->add_route( method => 'get', regexp => '/', code => sub {'/foo/bar'} ); $app->add_route( method => 'get', regexp => '/second', code => sub {'/foo/bar/second'} ); } ); }, ); # to make sure the lexical prefix did not crash anything $app->add_route( method => 'get', regexp => '/root', code => sub {'/root'} ); # make sure a meaningless lexical prefix is ignored $app->lexical_prefix( '/' => sub { $app->add_route( method => 'get', regexp => '/somewhere', code => sub {'/somewhere'}, ); } ); for my $path ( '/foo/', '/foo/second', '/foo/bar/second', '/root', '/somewhere' ) { my $env = { REQUEST_METHOD => 'GET', PATH_INFO => $path, }; my $resp = $dispatcher->dispatch($env); is $resp->[0], 200, 'got a 200'; is $resp->[2][0], $path, 'got expected route'; } note "test a failure in the callback of a lexical prefix"; like( exception { $app->lexical_prefix( '/test' => sub { Failure->game_over() } ); }, qr{Unable to run the callback for prefix '/test': Can't locate object method "game_over" via package "Failure"}, "caught an exception in the lexical prefix callback", ); $app->add_hook( Dancer2::Core::Hook->new( name => 'before', code => sub {1}, ) ); $app->add_hook( Dancer2::Core::Hook->new( name => 'before', code => sub { Foo->failure; }, ) ); $app->compile_hooks; my $env = { REQUEST_METHOD => 'GET', PATH_INFO => '/', }; like( $dispatcher->dispatch($env)->[2][0], qr/Exception caught in 'core.app.before_request' filter: Hook error: Can't locate object method "failure"/, 'before filter nonexistent method failure', ); $app->replace_hook( 'core.app.before_request', [ sub {1} ] ); $app->compile_hooks; $env = { REQUEST_METHOD => 'GET', PATH_INFO => '/', }; # test duplicate routes when the path is a regex $app = Dancer2::Core::App->new( name => 'main' ); my $regexp_route = { method => 'get', 'regexp' => qr!/(\d+)!, code => sub {1} }; $app->add_route(%$regexp_route); # try to get an invalid engine eval {$app->engine('foo')}; like( $@, qr/^Engine 'foo' is not supported/, "Engine 'foo' does not exist", ); my $tmpl_engine = $app->engine('template'); ok $tmpl_engine, "Template engine is defined"; ok !$app->has_serializer_engine, "Serializer engine does not exist"; is_deeply( $app->_get_config_for_engine('NonExistent'), {}, 'Empty configuration for nonexistent engine', ); # TODO: not such an intelligent check, this one... # set configuration for an engine $app->config->{'engines'}{'template'}{'Tiny'}{'hello'} = 'world'; is_deeply( $app->_get_config_for_engine( template => 'Tiny', $app->config ), { hello => 'world' }, '_get_config_for_engine can find the right configuration', ); is( File::Spec->canonpath( $app->caller ), File::Spec->catfile(t => 'app.t'), 'Correct caller for app', ); done_testing; LICENSE100644001751001751 4366313171470520 13753 0ustar00jasonjason000000000000Dancer2-0.205002This software is copyright (c) 2017 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. Terms of the Perl programming language system itself a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or b) the "Artistic License" --- The GNU General Public License, Version 1, February 1989 --- This software is Copyright (c) 2017 by Alexis Sukrieh. This is free software, licensed under: The GNU General Public License, Version 1, February 1989 GNU GENERAL PUBLIC LICENSE Version 1, February 1989 Copyright (C) 1989 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The license agreements of most software companies try to keep users at the mercy of those companies. By contrast, our General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. The General Public License applies to the Free Software Foundation's software and to any other program whose authors commit to using it. You can use it for your programs, too. When we speak of free software, we are referring to freedom, not price. Specifically, the General Public License is designed to make sure that you have the freedom to give away or sell copies of free software, that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of a such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must tell them their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any work containing the Program or a portion of it, either verbatim or with modifications. Each licensee is addressed as "you". 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this General Public License and to the absence of any warranty; and give any other recipients of the Program a copy of this General Public License along with the Program. You may charge a fee for the physical act of transferring a copy. 2. You may modify your copy or copies of the Program or any portion of it, and copy and distribute such modifications under the terms of Paragraph 1 above, provided that you also do the following: a) cause the modified files to carry prominent notices stating that you changed the files and the date of any change; and b) cause the whole of any work that you distribute or publish, that in whole or in part contains the Program or any part thereof, either with or without modifications, to be licensed at no charge to all third parties under the terms of this General Public License (except that you may choose to grant warranty protection to some or all third parties, at your option). c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the simplest and most usual way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this General Public License. d) You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. Mere aggregation of another independent work with the Program (or its derivative) on a volume of a storage or distribution medium does not bring the other work under the scope of these terms. 3. You may copy and distribute the Program (or a portion or derivative of it, under Paragraph 2) in object code or executable form under the terms of Paragraphs 1 and 2 above provided that you also do one of the following: a) accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Paragraphs 1 and 2 above; or, b) accompany it with a written offer, valid for at least three years, to give any third party free (except for a nominal charge for the cost of distribution) a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Paragraphs 1 and 2 above; or, c) accompany it with the information you received as to where the corresponding source code may be obtained. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form alone.) Source code for a work means the preferred form of the work for making modifications to it. For an executable file, complete source code means all the source code for all modules it contains; but, as a special exception, it need not include source code for modules which are standard libraries that accompany the operating system on which the executable file runs, or for standard header files or definitions files that accompany that operating system. 4. You may not copy, modify, sublicense, distribute or transfer the Program except as expressly provided under this General Public License. Any attempt otherwise to copy, modify, sublicense, distribute or transfer the Program is void, and will automatically terminate your rights to use the Program under this License. However, parties who have received copies, or rights to use copies, from you under this General Public License will not have their licenses terminated so long as such parties remain in full compliance. 5. By copying, distributing or modifying the Program (or any work based on the Program) you indicate your acceptance of this license to do so, and all its terms and conditions. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. 7. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of the license which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the license, you may choose any version ever published by the Free Software Foundation. 8. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to humanity, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19xx name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (a program to direct compilers to make passes at assemblers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice That's all there is to it! --- The Artistic License 1.0 --- This software is Copyright (c) 2017 by Alexis Sukrieh. This is free software, licensed under: The Artistic License 1.0 The Artistic License Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: - "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. - "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder. - "Copyright Holder" is whoever is named in the copyright or copyrights for the package. - "You" is you, if you're thinking about copying or distributing this Package. - "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) - "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as ftp.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) accompany any non-standard executables with their corresponding Standard Version executables, giving the non-standard executables non-standard names, and clearly documenting the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this Package. 7. C or perl subroutines supplied by you and linked into this Package shall not be considered part of this Package. 8. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End cpanfile100644001751001751 551013171470520 14417 0ustar00jasonjason000000000000Dancer2-0.205002requires 'App::Cmd::Setup'; requires 'Attribute::Handlers'; requires 'Carp'; requires 'Config::Any'; requires 'Digest::SHA'; requires 'Encode'; requires 'Exporter', '5.57'; requires 'Exporter::Tiny'; requires 'File::Basename'; requires 'File::Copy'; requires 'File::Find'; requires 'File::Path'; requires 'File::ShareDir'; requires 'File::Spec'; requires 'File::Temp'; requires 'Hash::Merge::Simple'; requires 'Hash::MultiValue'; requires 'HTTP::Body'; requires 'HTTP::Date'; requires 'HTTP::Headers::Fast'; requires 'HTTP::Tiny'; requires 'Import::Into'; requires 'JSON::MaybeXS'; requires 'List::Util', '1.29'; # 1.29 has the pair* functions requires 'MIME::Base64', '3.13'; # 3.13 has the URL safe variants requires 'Module::Runtime'; requires 'Moo', '2.000000'; requires 'Moo::Role'; requires 'parent'; requires 'Plack', '1.0035'; requires 'Plack::Middleware::FixMissingBodyInRedirect'; requires 'Plack::Middleware::RemoveRedundantBody'; requires 'POSIX'; requires 'Ref::Util'; requires 'Return::MultiLevel'; requires 'Role::Tiny', '2.000000'; requires 'Safe::Isa'; requires 'Sub::Quote'; requires 'Template'; requires 'Template::Tiny'; requires 'Test::Builder'; requires 'Test::More'; requires 'Type::Tiny', '1.000006'; requires 'URI::Escape'; # Minimum version of YAML is needed due to: # - https://github.com/PerlDancer/Dancer2/issues/899 # Excluded 1.16 is needs due to: # - http://www.cpantesters.org/cpan/report/25911c10-4199-11e6-8d7d-86c55bc2a771 # - http://www.cpantesters.org/cpan/report/284ac158-419a-11e6-9a35-e3e15bc2a771 requires 'YAML', '0.86'; conflicts 'YAML', '1.16'; recommends 'CGI::Deurl::XS'; recommends 'Class::XSAccessor'; recommends 'Cpanel::JSON::XS'; recommends 'Crypt::URandom'; recommends 'HTTP::XSCookies', '0.000007'; # GH#1332 if old HTTP::XSCookies is installed we need to upgrade eval "require HTTP::XSCookies" && requires 'HTTP::XSCookies', '0.000007'; recommends 'HTTP::XSHeaders'; recommends 'Math::Random::ISAAC::XS'; recommends 'MooX::TypeTiny'; recommends 'Pod::Simple::Search'; recommends 'Pod::Simple::SimpleTree'; recommends 'Scope::Upper'; recommends 'Type::Tiny::XS'; recommends 'URL::Encode::XS'; recommends 'YAML::XS'; suggests 'Fcntl'; suggests 'MIME::Types'; test_requires 'Capture::Tiny', '0.12'; test_requires 'HTTP::Body'; test_requires 'HTTP::Cookies'; test_requires 'HTTP::Headers'; test_requires 'Template'; test_requires 'Test::Builder'; test_requires 'Test::EOL'; test_requires 'Test::Fatal'; test_requires 'Test::More'; test_requires 'Test::More', '0.92'; author_requires 'AnyEvent'; author_requires 'CBOR::XS'; author_requires 'Class::Method::Modifiers'; author_requires 'Dist::Zilla::Plugin::Test::UnusedVars'; author_requires 'Perl::Tidy'; author_requires 'Test::Memory::Cycle'; author_requires 'Test::MockTime'; author_requires 'Test::Perl::Critic'; author_requires 'Test::Whitespaces'; author_requires 'YAML::XS'; time.t100644001751001751 413413171470520 14302 0ustar00jasonjason000000000000Dancer2-0.205002/t# time.t use strict; use warnings; use Test::More; my $mocked_epoch = 1355676244; # "Sun, 16-Dec-2012 16:44:04 GMT" # The order is important! eval { require Test::MockTime; 1; } or plan skip_all => 'Test::MockTime not present'; Test::MockTime::set_fixed_time($mocked_epoch); require Dancer2::Core::Time; my @tests = ( [ "1h" => 3600 => "Sun, 16-Dec-2012 17:44:04 GMT" ], [ "1 hour" => 3600 => "Sun, 16-Dec-2012 17:44:04 GMT" ], [ "+1 hour" => 3600 => "Sun, 16-Dec-2012 17:44:04 GMT" ], [ "-1h" => -3600 => "Sun, 16-Dec-2012 15:44:04 GMT" ], [ "1 hours" => 3600 => "Sun, 16-Dec-2012 17:44:04 GMT" ], [ "1d" => ( 3600 * 24 ) => "Mon, 17-Dec-2012 16:44:04 GMT" ], [ "1 day" => ( 3600 * 24 ) => "Mon, 17-Dec-2012 16:44:04 GMT" ], ); foreach my $test (@tests) { my ( $expr, $secs, $gmt_string ) = @$test; subtest "Expression: \"$expr\"" => sub { my $t = Dancer2::Core::Time->new( expression => $expr ); is $t->seconds, $secs, "\"$expr\" is $secs seconds"; is $t->epoch, ( $t->seconds + $mocked_epoch ), "... its epoch is " . $t->epoch; is $t->gmt_string, $gmt_string, "... and its GMT string is $gmt_string"; }; } subtest "Forcing another epoch in the object should work" => sub { my $t = Dancer2::Core::Time->new( epoch => 1, expression => "1h" ); is $t->seconds, 3600, "...1h is still 3600 seconds"; is $t->epoch, 1, "... epoch is 1"; is $t->gmt_string, 'Thu, 01-Jan-1970 00:00:01 GMT', "... and is expressed as Thu, 01-Jan-1970 00:00:01 GMT"; }; subtest "unparsable strings should be kept" => sub { for my $t ( [ "something silly", "something silly", "something silly" ], [ "+2 something", "+2 something", "+2 something" ], ) { my ( $expr, $secs, $gmt ) = @$t; my $t = Dancer2::Core::Time->new( expression => $expr ); is $t->seconds, $secs, "\"$expr\" is $secs seconds"; is $t->epoch, $expr, "... its epoch is $expr"; is $t->gmt_string, $gmt, "... and its GMT string is $gmt"; } }; done_testing; mime.t100644001751001751 230113171470520 14265 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More tests => 12; BEGIN { use_ok 'Dancer2::Core::MIME'; } my $mime = Dancer2::Core::MIME->new(); is_deeply( $mime->custom_types, {}, 'user defined mime_types are empty' ); $mime->add_type( foo => 'text/foo' ); is_deeply( $mime->custom_types, { foo => 'text/foo' }, 'text/foo is saved' ); is( $mime->for_name('foo'), 'text/foo', 'mime type foo is found' ); $mime->add_alias( bar => 'foo' ); is( $mime->for_name('bar'), 'text/foo', 'mime type bar is found' ); is( $mime->for_file('foo.bar'), 'text/foo', 'mime type for extension .bar is found' ); is( $mime->for_file('foobar'), $mime->default, 'mime type for no extension is the default' ); is( $mime->add_alias( xpto => 'BAR' ), 'text/foo', 'mime gets correctly lowercased for user types' ); is $mime->add_alias( xpto => 'SVG' ) => 'image/svg+xml', 'mime gets correctly lowercased for system types'; is $mime->add_alias( zbr => 'baz' ) => $mime->default, 'alias of unknown mime type gets default mime type'; is $mime->name_or_type("text/zbr") => "text/zbr", 'name_or_type does not change if it seems a mime type string'; is $mime->name_or_type("svg") => "image/svg+xml", 'name_or_type knows svg'; vars.t100644001751001751 112613171470520 14315 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; plan tests => 3; { use Dancer2; hook before => sub { var( "xpto" => "foo" ); vars->{zbr} = 'ugh'; }; get '/bar' => sub { var("xpto"); }; get '/baz' => sub { vars->{zbr}; }; } my $app = __PACKAGE__->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/bar' )->content, 'foo', 'foo' ); is( $cb->( GET '/baz' )->content, 'ugh', 'ugh' ); }; META.yml100644001751001751 1674213171470520 14215 0ustar00jasonjason000000000000Dancer2-0.205002--- abstract: 'Lightweight yet powerful web application framework' author: - 'Dancer Core Developers' build_requires: Capture::Tiny: '0.12' ExtUtils::MakeMaker: '7.1101' File::Spec: '0' HTTP::Body: '0' HTTP::Cookies: '0' HTTP::Headers: '0' IO::Handle: '0' IPC::Open3: '0' Template: '0' Test::Builder: '0' Test::EOL: '0' Test::Fatal: '0' Test::More: '0.92' configure_requires: ExtUtils::MakeMaker: '7.1101' File::ShareDir::Install: '0.06' conflicts: YAML: '1.16' dynamic_config: 0 generated_by: 'Dist::Zilla version 6.009, CPAN::Meta::Converter version 2.150005' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Dancer2 provides: Dancer2: file: lib/Dancer2.pm version: '0.205002' Dancer2::CLI: file: lib/Dancer2/CLI.pm version: '0.205002' Dancer2::CLI::Command::gen: file: lib/Dancer2/CLI/Command/gen.pm version: '0.205002' Dancer2::CLI::Command::version: file: lib/Dancer2/CLI/Command/version.pm version: '0.205002' Dancer2::Core: file: lib/Dancer2/Core.pm version: '0.205002' Dancer2::Core::App: file: lib/Dancer2/Core/App.pm version: '0.205002' Dancer2::Core::Cookie: file: lib/Dancer2/Core/Cookie.pm version: '0.205002' Dancer2::Core::DSL: file: lib/Dancer2/Core/DSL.pm version: '0.205002' Dancer2::Core::Dispatcher: file: lib/Dancer2/Core/Dispatcher.pm version: '0.205002' Dancer2::Core::Error: file: lib/Dancer2/Core/Error.pm version: '0.205002' Dancer2::Core::Factory: file: lib/Dancer2/Core/Factory.pm version: '0.205002' Dancer2::Core::HTTP: file: lib/Dancer2/Core/HTTP.pm version: '0.205002' Dancer2::Core::Hook: file: lib/Dancer2/Core/Hook.pm version: '0.205002' Dancer2::Core::MIME: file: lib/Dancer2/Core/MIME.pm version: '0.205002' Dancer2::Core::Request: file: lib/Dancer2/Core/Request.pm version: '0.205002' Dancer2::Core::Request::Upload: file: lib/Dancer2/Core/Request/Upload.pm version: '0.205002' Dancer2::Core::Response: file: lib/Dancer2/Core/Response.pm version: '0.205002' Dancer2::Core::Response::Delayed: file: lib/Dancer2/Core/Response/Delayed.pm version: '0.205002' Dancer2::Core::Role::ConfigReader: file: lib/Dancer2/Core/Role/ConfigReader.pm version: '0.205002' Dancer2::Core::Role::DSL: file: lib/Dancer2/Core/Role/DSL.pm version: '0.205002' Dancer2::Core::Role::Engine: file: lib/Dancer2/Core/Role/Engine.pm version: '0.205002' Dancer2::Core::Role::Handler: file: lib/Dancer2/Core/Role/Handler.pm version: '0.205002' Dancer2::Core::Role::HasLocation: file: lib/Dancer2/Core/Role/HasLocation.pm version: '0.205002' Dancer2::Core::Role::Hookable: file: lib/Dancer2/Core/Role/Hookable.pm version: '0.205002' Dancer2::Core::Role::Logger: file: lib/Dancer2/Core/Role/Logger.pm version: '0.205002' Dancer2::Core::Role::Serializer: file: lib/Dancer2/Core/Role/Serializer.pm version: '0.205002' Dancer2::Core::Role::SessionFactory: file: lib/Dancer2/Core/Role/SessionFactory.pm version: '0.205002' Dancer2::Core::Role::SessionFactory::File: file: lib/Dancer2/Core/Role/SessionFactory/File.pm version: '0.205002' Dancer2::Core::Role::StandardResponses: file: lib/Dancer2/Core/Role/StandardResponses.pm version: '0.205002' Dancer2::Core::Role::Template: file: lib/Dancer2/Core/Role/Template.pm version: '0.205002' Dancer2::Core::Route: file: lib/Dancer2/Core/Route.pm version: '0.205002' Dancer2::Core::Runner: file: lib/Dancer2/Core/Runner.pm version: '0.205002' Dancer2::Core::Session: file: lib/Dancer2/Core/Session.pm version: '0.205002' Dancer2::Core::Time: file: lib/Dancer2/Core/Time.pm version: '0.205002' Dancer2::Core::Types: file: lib/Dancer2/Core/Types.pm version: '0.205002' Dancer2::FileUtils: file: lib/Dancer2/FileUtils.pm version: '0.205002' Dancer2::Handler::AutoPage: file: lib/Dancer2/Handler/AutoPage.pm version: '0.205002' Dancer2::Handler::File: file: lib/Dancer2/Handler/File.pm version: '0.205002' Dancer2::Logger::Capture: file: lib/Dancer2/Logger/Capture.pm version: '0.205002' Dancer2::Logger::Capture::Trap: file: lib/Dancer2/Logger/Capture/Trap.pm version: '0.205002' Dancer2::Logger::Console: file: lib/Dancer2/Logger/Console.pm version: '0.205002' Dancer2::Logger::Diag: file: lib/Dancer2/Logger/Diag.pm version: '0.205002' Dancer2::Logger::File: file: lib/Dancer2/Logger/File.pm version: '0.205002' Dancer2::Logger::Note: file: lib/Dancer2/Logger/Note.pm version: '0.205002' Dancer2::Logger::Null: file: lib/Dancer2/Logger/Null.pm version: '0.205002' Dancer2::Plugin: file: lib/Dancer2/Plugin.pm version: '0.205002' Dancer2::Serializer::Dumper: file: lib/Dancer2/Serializer/Dumper.pm version: '0.205002' Dancer2::Serializer::JSON: file: lib/Dancer2/Serializer/JSON.pm version: '0.205002' Dancer2::Serializer::Mutable: file: lib/Dancer2/Serializer/Mutable.pm version: '0.205002' Dancer2::Serializer::YAML: file: lib/Dancer2/Serializer/YAML.pm version: '0.205002' Dancer2::Session::Simple: file: lib/Dancer2/Session/Simple.pm version: '0.205002' Dancer2::Session::YAML: file: lib/Dancer2/Session/YAML.pm version: '0.205002' Dancer2::Template::Implementation::ForkedTiny: file: lib/Dancer2/Template/Implementation/ForkedTiny.pm version: '0.205002' Dancer2::Template::Simple: file: lib/Dancer2/Template/Simple.pm version: '0.205002' Dancer2::Template::TemplateToolkit: file: lib/Dancer2/Template/TemplateToolkit.pm version: '0.205002' Dancer2::Template::Tiny: file: lib/Dancer2/Template/Tiny.pm version: '0.205002' Dancer2::Test: file: lib/Dancer2/Test.pm version: '0.205002' recommends: CGI::Deurl::XS: '0' Class::XSAccessor: '0' Cpanel::JSON::XS: '0' Crypt::URandom: '0' HTTP::XSCookies: '0.000007' HTTP::XSHeaders: '0' Math::Random::ISAAC::XS: '0' MooX::TypeTiny: '0' Pod::Simple::Search: '0' Pod::Simple::SimpleTree: '0' Scope::Upper: '0' Type::Tiny::XS: '0' URL::Encode::XS: '0' YAML::XS: '0' requires: App::Cmd::Setup: '0' Attribute::Handlers: '0' Carp: '0' Config::Any: '0' Digest::SHA: '0' Encode: '0' Exporter: '5.57' Exporter::Tiny: '0' File::Basename: '0' File::Copy: '0' File::Find: '0' File::Path: '0' File::ShareDir: '0' File::Spec: '0' File::Temp: '0' HTTP::Body: '0' HTTP::Date: '0' HTTP::Headers::Fast: '0' HTTP::Tiny: '0' HTTP::XSCookies: '0.000007' Hash::Merge::Simple: '0' Hash::MultiValue: '0' Import::Into: '0' JSON::MaybeXS: '0' List::Util: '1.29' MIME::Base64: '3.13' Module::Runtime: '0' Moo: '2.000000' Moo::Role: '0' POSIX: '0' Plack: '1.0035' Plack::Middleware::FixMissingBodyInRedirect: '0' Plack::Middleware::RemoveRedundantBody: '0' Ref::Util: '0' Return::MultiLevel: '0' Role::Tiny: '2.000000' Safe::Isa: '0' Sub::Quote: '0' Template: '0' Template::Tiny: '0' Test::Builder: '0' Test::More: '0.92' Type::Tiny: '1.000006' URI::Escape: '0' YAML: '0.86' parent: '0' resources: IRC: irc://irc.perl.org/#dancer WebIRC: https://chat.mibbit.com/#dancer@irc.perl.org bugtracker: https://github.com/PerlDancer/Dancer2/issues homepage: http://perldancer.org/ repository: git://github.com/PerlDancer/Dancer2.git version: '0.205002' x_serialization_backend: 'YAML::Tiny version 1.70' MANIFEST100644001751001751 2402013171470520 14061 0ustar00jasonjason000000000000Dancer2-0.205002# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.009. AUTHORS Changes GitGuide.md LICENSE MANIFEST META.json META.yml Makefile.PL cpanfile lib/Dancer2.pm lib/Dancer2/CLI.pm lib/Dancer2/CLI/Command/gen.pm lib/Dancer2/CLI/Command/version.pm lib/Dancer2/Config.pod lib/Dancer2/Cookbook.pod lib/Dancer2/Core.pm lib/Dancer2/Core/App.pm lib/Dancer2/Core/Cookie.pm lib/Dancer2/Core/DSL.pm lib/Dancer2/Core/Dispatcher.pm lib/Dancer2/Core/Error.pm lib/Dancer2/Core/Factory.pm lib/Dancer2/Core/HTTP.pm lib/Dancer2/Core/Hook.pm lib/Dancer2/Core/MIME.pm lib/Dancer2/Core/Request.pm lib/Dancer2/Core/Request/Upload.pm lib/Dancer2/Core/Response.pm lib/Dancer2/Core/Response/Delayed.pm lib/Dancer2/Core/Role/ConfigReader.pm lib/Dancer2/Core/Role/DSL.pm lib/Dancer2/Core/Role/Engine.pm lib/Dancer2/Core/Role/Handler.pm lib/Dancer2/Core/Role/HasLocation.pm lib/Dancer2/Core/Role/Hookable.pm lib/Dancer2/Core/Role/Logger.pm lib/Dancer2/Core/Role/Serializer.pm lib/Dancer2/Core/Role/SessionFactory.pm lib/Dancer2/Core/Role/SessionFactory/File.pm lib/Dancer2/Core/Role/StandardResponses.pm lib/Dancer2/Core/Role/Template.pm lib/Dancer2/Core/Route.pm lib/Dancer2/Core/Runner.pm lib/Dancer2/Core/Session.pm lib/Dancer2/Core/Time.pm lib/Dancer2/Core/Types.pm lib/Dancer2/FileUtils.pm lib/Dancer2/Handler/AutoPage.pm lib/Dancer2/Handler/File.pm lib/Dancer2/Logger/Capture.pm lib/Dancer2/Logger/Capture/Trap.pm lib/Dancer2/Logger/Console.pm lib/Dancer2/Logger/Diag.pm lib/Dancer2/Logger/File.pm lib/Dancer2/Logger/Note.pm lib/Dancer2/Logger/Null.pm lib/Dancer2/Manual.pod lib/Dancer2/Manual/Deployment.pod lib/Dancer2/Manual/Migration.pod lib/Dancer2/Manual/Testing.pod lib/Dancer2/Plugin.pm lib/Dancer2/Plugins.pod lib/Dancer2/Policy.pod lib/Dancer2/Serializer/Dumper.pm lib/Dancer2/Serializer/JSON.pm lib/Dancer2/Serializer/Mutable.pm lib/Dancer2/Serializer/YAML.pm lib/Dancer2/Session/Simple.pm lib/Dancer2/Session/YAML.pm lib/Dancer2/Template/Implementation/ForkedTiny.pm lib/Dancer2/Template/Simple.pm lib/Dancer2/Template/TemplateToolkit.pm lib/Dancer2/Template/Tiny.pm lib/Dancer2/Test.pm lib/Dancer2/Tutorial.pod script/dancer2 share/skel/.dancer share/skel/MANIFEST.SKIP share/skel/Makefile.PL share/skel/bin/+app.psgi share/skel/config.yml share/skel/cpanfile share/skel/environments/development.yml share/skel/environments/production.yml share/skel/lib/AppFile.pm share/skel/public/+dispatch.cgi share/skel/public/+dispatch.fcgi share/skel/public/404.html share/skel/public/500.html share/skel/public/css/error.css share/skel/public/css/style.css share/skel/public/favicon.ico share/skel/public/images/perldancer-bg.jpg share/skel/public/images/perldancer.jpg share/skel/public/javascripts/jquery.js share/skel/t/001_base.t share/skel/t/002_index_route.t share/skel/views/index.tt share/skel/views/layouts/main.tt t/00-compile.t t/00-report-prereqs.dd t/00-report-prereqs.t t/app.t t/app/t1/bin/app.psgi t/app/t1/config.yml t/app/t1/lib/App1.pm t/app/t1/lib/Sub/App2.pm t/app/t2/.dancer t/app/t2/config.yml t/app/t2/lib/App3.pm t/app_alone.t t/author-no-tabs.t t/author-pod-syntax.t t/auto_page.t t/caller.t t/charset_server.t t/classes/Dancer2-Core-Factory/new.t t/classes/Dancer2-Core-Hook/new.t t/classes/Dancer2-Core-Request/new.t t/classes/Dancer2-Core-Request/serializers.t t/classes/Dancer2-Core-Response-Delayed/after_hooks.t t/classes/Dancer2-Core-Response-Delayed/new.t t/classes/Dancer2-Core-Response/new_from.t t/classes/Dancer2-Core-Role-Engine/with.t t/classes/Dancer2-Core-Role-Handler/with.t t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl t/classes/Dancer2-Core-Role-HasLocation/with.t t/classes/Dancer2-Core-Role-Serializer/with.t t/classes/Dancer2-Core-Role-StandardResponses/with.t t/classes/Dancer2-Core-Route/base.t t/classes/Dancer2-Core-Route/deprecated_param_keys.t t/classes/Dancer2-Core-Route/match.t t/classes/Dancer2-Core-Runner/environment.t t/classes/Dancer2-Core-Runner/new.t t/classes/Dancer2-Core-Runner/psgi_app.t t/classes/Dancer2-Core/camelize.t t/classes/Dancer2/import-pragmas.t t/classes/Dancer2/import.t t/config.yml t/config/config.yml t/config/environments/failure.yml t/config/environments/merging.yml t/config/environments/production.yml t/config/environments/staging.json t/config2/config.yml t/config2/config_local.yml t/config2/environments/lconfig.yml t/config2/environments/lconfig_local.yml t/config_multiapp.t t/config_reader.t t/config_settings.t t/context-in-before.t t/cookie.t t/corpus/pretty/505.tt t/corpus/pretty/relative.tt t/corpus/pretty_public/404.html t/corpus/pretty_public/510.html t/corpus/static/1x1.png t/corpus/static/index.html t/custom_dsl.t t/dancer-test.t t/dancer-test/config.yml t/deserialize.t t/disp_named_capture.t t/dispatcher.t t/dsl/any.t t/dsl/app.t t/dsl/content.t t/dsl/delayed.t t/dsl/error_template.t t/dsl/extend.t t/dsl/extend_config/config.yml t/dsl/halt.t t/dsl/halt_with_param.t t/dsl/json.t t/dsl/parameters.t t/dsl/pass.t t/dsl/path.t t/dsl/request.t t/dsl/route_retvals.t t/dsl/send_as.t t/dsl/send_file.t t/dsl/splat.t t/dsl/to_app.t t/engine.t t/error.t t/factory.t t/file_utils.t t/forward.t t/forward_before_hook.t t/forward_hmv_params.t t/forward_test_tcp.t t/hooks.t t/http_methods.t t/http_status.t t/issues/config.yml t/issues/gh-1013/gh-1013.t t/issues/gh-1013/views/t.tt t/issues/gh-1046/config.yml t/issues/gh-1046/gh-1046.t t/issues/gh-1070.t t/issues/gh-1098.t t/issues/gh-1216/gh-1216.t t/issues/gh-1216/lib/App.pm t/issues/gh-1216/lib/App/Extra.pm t/issues/gh-1216/lib/Dancer2/Plugin/Null.pm t/issues/gh-1226/gh-1226.t t/issues/gh-1226/lib/App.pm t/issues/gh-1226/lib/App/Extra.pm t/issues/gh-1226/lib/Dancer2/Plugin/Test/AccessDSL.pm t/issues/gh-1230/gh-1230.t t/issues/gh-1230/lib/App.pm t/issues/gh-1230/lib/App/Extra.pm t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessDSL.pm t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessPluginDSL.pm t/issues/gh-1232.t t/issues/gh-596.t t/issues/gh-634.t t/issues/gh-639/fails/.dancer t/issues/gh-639/fails/config.yml t/issues/gh-639/fails/issue.t t/issues/gh-639/succeeds/.dancer t/issues/gh-639/succeeds/config.yml t/issues/gh-639/succeeds/issue.t t/issues/gh-650/gh-650.t t/issues/gh-650/views/environment_setting.tt t/issues/gh-723.t t/issues/gh-730.t t/issues/gh-762.t t/issues/gh-762/views/404.tt t/issues/gh-794.t t/issues/gh-797.t t/issues/gh-799.t t/issues/gh-811.t t/issues/gh-931.t t/issues/gh-936.t t/issues/gh-936/views/error.tt t/issues/gh-944.t t/issues/gh-975/config.yml t/issues/gh-975/gh-975.t t/issues/gh-975/test_public_dir/test.txt t/issues/memleak/die_in_hooks.t t/issues/vars-in-forward.t t/lib/App1.pm t/lib/App2.pm t/lib/Dancer2/Plugin/Bar.pm t/lib/Dancer2/Plugin/DancerPlugin.pm t/lib/Dancer2/Plugin/DefineKeywords.pm t/lib/Dancer2/Plugin/EmptyPlugin.pm t/lib/Dancer2/Plugin/Foo.pm t/lib/Dancer2/Plugin/FooPlugin.pm t/lib/Dancer2/Plugin/Hookee.pm t/lib/Dancer2/Plugin/OnPluginImport.pm t/lib/Dancer2/Plugin/PluginWithImport.pm t/lib/Dancer2/Plugin/Polite.pm t/lib/Dancer2/Session/SimpleNoChangeId.pm t/lib/Foo.pm t/lib/MyDancerDSL.pm t/lib/PoC/Plugin/Polite.pm t/lib/SubApp1.pm t/lib/SubApp2.pm t/lib/TestApp.pm t/lib/TestPod.pm t/lib/poc.pm t/lib/poc2.pm t/log_die_before_hook.t t/log_levels.t t/logger.t t/logger_console.t t/memory_cycles.t t/mime.t t/multi_apps.t t/multi_apps_forward.t t/multiapp_template_hooks.t t/named_apps.t t/plugin2/basic-2.t t/plugin2/basic.t t/plugin2/define-keywords.t t/plugin2/find_plugin.t t/plugin2/from-config.t t/plugin2/hooks.t t/plugin2/inside-plugin.t t/plugin2/keywords-hooks-namespace.t t/plugin2/memory_cycles.t t/plugin2/no-app-munging.t t/plugin2/no-clobbering.t t/plugin2/no-config.t t/plugin2/with-plugins.t t/plugin_import.t t/plugin_multiple_apps.t t/plugin_register.t t/plugin_syntax.t t/prepare_app.t t/psgi_app.t t/psgi_app_forward_and_pass.t t/public/file.txt t/redirect.t t/release-distmeta.t t/request.t t/request_make_forward_to.t t/request_upload.t t/response.t t/roles/hook.t t/route-pod-coverage/route-pod-coverage.t t/scope_problems/config.yml t/scope_problems/dispatcher_internal_request.t t/scope_problems/keywords_before_template_hook.t t/scope_problems/session_is_cleared.t t/scope_problems/views/500.tt t/scope_problems/with_return_dies.t t/serializer.t t/serializer_json.t t/serializer_mutable.t t/session_bad_client_cookie.t t/session_config.t t/session_engines.t t/session_forward.t t/session_hooks.t t/session_hooks_no_change_id.t t/session_in_template.t t/session_lifecycle.t t/session_object.t t/shared_engines.t t/static_content.t t/template.t t/template_default_tokens.t t/template_ext.t t/template_name.t t/template_simple.t t/template_tiny/01_compile.t t/template_tiny/02_trivial.t t/template_tiny/03_samples.t t/template_tiny/04_compat.t t/template_tiny/05_preparse.t t/template_tiny/samples/01_hello.tt t/template_tiny/samples/01_hello.txt t/template_tiny/samples/01_hello.var t/template_tiny/samples/02_null.tt t/template_tiny/samples/02_null.txt t/template_tiny/samples/02_null.var t/template_tiny/samples/03_chomp.tt t/template_tiny/samples/03_chomp.txt t/template_tiny/samples/03_chomp.var t/template_tiny/samples/04_nested.tt t/template_tiny/samples/04_nested.txt t/template_tiny/samples/04_nested.var t/template_tiny/samples/05_condition.tt t/template_tiny/samples/05_condition.txt t/template_tiny/samples/05_condition.var t/template_tiny/samples/06_object.tt t/template_tiny/samples/06_object.txt t/template_tiny/samples/06_object.var t/template_tiny/samples/07_nesting.tt t/template_tiny/samples/07_nesting.txt t/template_tiny/samples/07_nesting.var t/template_tiny/samples/08_foreach.tt t/template_tiny/samples/08_foreach.txt t/template_tiny/samples/08_foreach.var t/template_tiny/samples/09_trim.tt t/template_tiny/samples/09_trim.txt t/template_tiny/samples/09_trim.var t/time.t t/types.t t/uri_for.t t/vars.t t/views/auto_page.tt t/views/beforetemplate.tt t/views/folder/page.tt t/views/index.tt t/views/layouts/main.tt t/views/session_in_template.tt t/views/template_simple_index.tt t/views/tokens.tt xt/perlcritic.rc xt/perltidy.rc xt/whitespace.t hooks.t100644001751001751 1406513171470520 14513 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use File::Spec; use Plack::Test; use HTTP::Request::Common; use Capture::Tiny 0.12 'capture_stderr'; use JSON::MaybeXS; eval { require Template; 1; } or plan skip_all => 'Template::Toolkit not present'; my $tests_flags = {}; { package App::WithSerializer; use Dancer2; use Ref::Util qw; set serializer => 'JSON'; my @hooks = qw( before_request after_request before_serializer after_serializer ); for my $hook (@hooks) { hook $hook => sub { $tests_flags->{$hook} ||= 0; $tests_flags->{$hook}++; }; } get '/' => sub { +{ "ok" => 1 } }; hook 'before_serializer' => sub { my ($data) = @_; # don't shift, want to alias.. if ( is_arrayref($data) ) { push( @{$data}, ( added_in_hook => 1 ) ); } elsif ( is_hashref($data) ) { $data->{'added_in_hook'} = 1; } else { $_[0] = +{ 'added_in_hook' => 1 }; } }; get '/forward' => sub { Test::More::note 'About to forward!'; forward '/' }; get '/redirect' => sub { redirect '/' }; get '/json' => sub { +[ foo => 42 ] }; get '/nothing' => sub { return }; } { package App::WithFile; use Dancer2; my @hooks = qw< before_file_render after_file_render >; for my $hook (@hooks) { hook $hook => sub { $tests_flags->{$hook} ||= 0; $tests_flags->{$hook}++; }; } get '/send_file' => sub { send_file( File::Spec->rel2abs(__FILE__), system_path => 1 ); }; } { package App::WithTemplate; use Dancer2; set template => 'tiny'; my @hooks = qw( before_template_render after_template_render ); for my $hook (@hooks) { hook $hook => sub { $tests_flags->{$hook} ||= 0; $tests_flags->{$hook}++; }; } get '/template' => sub { template \"PLOP"; }; } { package App::WithIntercept; use Dancer2; get '/intercepted' => sub {'not intercepted'}; hook before => sub { response->content('halted by before'); halt; }; } { package App::WithError; use Dancer2; my @hooks = qw( on_route_exception ); for my $hook (@hooks) { hook $hook => sub { $tests_flags->{$hook} ||= 0; $tests_flags->{$hook}++; }; } get '/route_exception' => sub {die 'this is a route exception'}; hook after => sub { # GH#540 - ensure setting default scalar does not # interfere with hook execution (aliasing) $_ = 42; }; hook on_route_exception => sub { my ($app, $error) = @_; ::is ref($app), 'Dancer2::Core::App'; ::like $error, qr/this is a route exception/; }; hook init_error => sub { my ($error) = @_; ::is ref($error), 'Dancer2::Core::Error'; }; hook before_error => sub { my ($error) = @_; ::is ref($error), 'Dancer2::Core::Error'; }; hook after_error => sub { my ($response) = @_; ::is ref($response), 'Dancer2::Core::Response'; ::ok !$response->is_halted; ::like $response->content, qr/Internal Server Error/; }; } subtest 'Request hooks' => sub { my $test = Plack::Test->create( App::WithSerializer->to_app ); $test->request( GET '/' ); is( $tests_flags->{before_request}, 1, "before_request was called" ); is( $tests_flags->{after_request}, 1, "after_request was called" ); is( $tests_flags->{before_serializer}, 1, "before_serializer was called" ); is( $tests_flags->{after_serializer}, 1, "after_serializer was called" ); is( $tests_flags->{before_file_render}, undef, "before_file_render undef" ); note 'after hook called once per request'; # Get current value of the 'after_request' tests flag. my $current = $tests_flags->{after_request}; $test->request( GET '/redirect' ); is( $tests_flags->{after_request}, ++$current, "after_request called after redirect", ); note 'Serializer hooks'; $test->request( GET '/forward' ); is( $tests_flags->{after_request}, ++$current, "after_request called only once after forward", ); my $res = $test->request( GET '/json' ); is( $res->content, '["foo",42,"added_in_hook",1]', 'Response serialized' ); is( $tests_flags->{before_serializer}, 4, 'before_serializer was called' ); is( $tests_flags->{after_serializer}, 4, 'after_serializer was called' ); is( $tests_flags->{before_file_render}, undef, "before_file_render undef" ); $res = $test->request( GET '/nothing' ); is( $res->content, '{"added_in_hook":1}', 'Before hook modified content' ); is( $tests_flags->{before_serializer}, 5, 'before_serializer was called with no content' ); is( $tests_flags->{after_serializer}, 5, 'after_serializer was called after content changes in hook' ); }; subtest 'file render hooks' => sub { my $test = Plack::Test->create( App::WithFile->to_app ); $test->request( GET '/send_file' ); is( $tests_flags->{before_file_render}, 1, "before_file_render was called" ); is( $tests_flags->{after_file_render}, 1, "after_file_render was called" ); }; subtest 'template render hook' => sub { my $test = Plack::Test->create( App::WithTemplate->to_app ); $test->request( GET '/template' ); is( $tests_flags->{before_template_render}, 1, "before_template_render was called", ); is( $tests_flags->{after_template_render}, 1, "after_template_render was called", ); }; subtest 'before can halt' => sub { my $test = Plack::Test->create( App::WithIntercept->to_app ); my $resp = $test->request( GET '/intercepted' ); is( $resp->content, 'halted by before' ); }; subtest 'route_exception' => sub { my $test = Plack::Test->create( App::WithError->to_app ); capture_stderr { $test->request( GET '/route_exception' ) }; }; done_testing; types.t100644001751001751 1303013171470520 14523 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More tests => 51; use Test::Fatal; use Dancer2::Core::Types; ok( exception { Str->(undef) }, 'Str does not accept undef value', ); is( exception { Str->('something') }, undef, 'Str', ); like( exception { Str->( { foo => 'something' } ) }, qr{Reference.+foo.+something.+did not pass type constraint.+Str}, 'Str', ); is( exception { Num->(34) }, undef, 'Num', ); ok( exception { Num->(undef) }, 'Num does not accept undef value', ); like( exception { Num->('not a number') }, qr{not a number.+did not pass type constraint.+Num}, 'Num fail', ); is( exception { Bool->(1) }, undef, 'Bool true value', ); is( exception { Bool->(0) }, undef, 'Bool false value', ); is( exception { Bool->(undef) }, undef, 'Bool does accepts undef value', ); like( exception { Bool->('2') }, qr{2.+did not pass type constraint.+Bool}, 'Bool fail', ); is( exception { RegexpRef->(qr{.*}) }, undef, 'Regexp', ); like( exception { RegexpRef->('/.*/') }, qr{\Q/.*/\E.+did not pass type constraint.+RegexpRef}, 'Regexp fail', ); ok( exception { RegexpRef->(undef) }, 'Regexp does not accept undef value', ); is( exception { HashRef->( { goo => 'le' } ) }, undef, 'HashRef', ); like( exception { HashRef->('/.*/') }, qr{\Q/.*/\E.+did not pass type constraint.+HashRef}, 'HashRef fail', ); ok( exception { HashRef->(undef) }, 'HashRef does not accept undef value', ); is( exception { ArrayRef->( [ 1, 2, 3, 4 ] ) }, undef, 'ArrayRef', ); like( exception { ArrayRef->('/.*/') }, qr{\Q/.*/\E.+did not pass type constraint.+ArrayRef}, 'ArrayRef fail', ); ok( exception { ArrayRef->(undef) }, 'ArrayRef does not accept undef value', ); is( exception { CodeRef->( sub {44} ); }, undef, 'CodeRef', ); like( exception { CodeRef->('/.*/') }, qr{\Q/.*/\E.+did not pass type constraint.+CodeRef}, 'CodeRef fail', ); ok( exception { CodeRef->(undef) }, 'CodeRef does not accept undef value', ); { package InstanceChecker::zad7; use Moo; use Dancer2::Core::Types; has foo => ( is => 'ro', isa => InstanceOf ['Foo'] ); } is( exception { InstanceChecker::zad7->new( foo => bless {}, 'Foo' ) }, undef, 'InstanceOf', ); like( exception { InstanceChecker::zad7->new( foo => bless {}, 'Bar' ) }, qr{Reference bless.+Bar.+not isa Foo}, 'InstanceOf fail', ); ok( exception { InstanceOf('Foo')->(undef) }, 'InstanceOf does not accept undef value', ); is( exception { Dancer2Prefix->('/foo') }, undef, 'Dancer2Prefix', ); like( exception { Dancer2Prefix->('bar/something') }, qr{bar/something.+did not pass type constraint.+Dancer2Prefix}, 'Dancer2Prefix fail', ); # see Dancer2Prefix definition, undef is a valid value like( exception { Dancer2Prefix->(undef) }, qr/Undef.+did not pass type constraint.+Dancer2Prefix/, 'Dancer2Prefix does not accept undef value', ); is( exception { Dancer2AppName->('Foo') }, undef, 'Dancer2AppName', ); is( exception { Dancer2AppName->('Foo::Bar') }, undef, 'Dancer2AppName', ); is( exception { Dancer2AppName->('Foo::Bar::Baz') }, undef, 'Dancer2AppName', ); like( exception { Dancer2AppName->('Foo:Bar') }, qr{Foo:Bar is not a Dancer2AppName}, 'Dancer2AppName fails with single colons', ); like( exception { Dancer2AppName->('Foo:::Bar') }, qr{Foo:::Bar is not a Dancer2AppName}, 'Dancer2AppName fails with tripe colons', ); like( exception { Dancer2AppName->('7Foo') }, qr{7Foo is not a Dancer2AppName}, 'Dancer2AppName fails with beginning number', ); like( exception { Dancer2AppName->('Foo::45Bar') }, qr{Foo::45Bar is not a Dancer2AppName}, 'Dancer2AppName fails with beginning number', ); like( exception { Dancer2AppName->('-F') }, qr{-F is not a Dancer2AppName}, 'Dancer2AppName fails with special character', ); like( exception { Dancer2AppName->('Foo::-') }, qr{Foo::- is not a Dancer2AppName}, 'Dancer2AppName fails with special character', ); like( exception { Dancer2AppName->('Foo^') }, qr{\QFoo^\E is not a Dancer2AppName}, 'Dancer2AppName fails with special character', ); ok( exception { Dancer2AppName->(undef) }, 'Dancer2AppName does not accept undef value', ); like( exception { Dancer2AppName->('') }, qr{Empty string is not a Dancer2AppName}, 'Dancer2AppName fails an empty string value', ); is( exception { Dancer2Method->('post') }, undef, 'Dancer2Method', ); like( exception { Dancer2Method->('POST') }, qr{POST.+did not pass type constraint.+Dancer2Method}, 'Dancer2Method fail', ); ok( exception { Dancer2Method->(undef) }, 'Dancer2Method does not accept undef value', ); is( exception { Dancer2HTTPMethod->('POST') }, undef, 'Dancer2HTTPMethod', ); like( exception { Dancer2HTTPMethod->('post') }, qr{post.+did not pass type constraint.+Dancer2HTTPMethod}, 'Dancer2HTTPMethod fail', ); ok( exception { Dancer2HTTPMethod->(undef) }, 'Dancer2Method does not accept undef value', ); use Dancer2::Core::Error; use Dancer2::Core::Hook; ok( exception { Hook->(undef) }, 'Hook does not accept undef value' ); ok(exception { Hook->(Dancer2::Core::Error->new) }, 'Hook does not Core::Error as value'); is( exception { Hook->(Dancer2::Core::Hook->new(name => 'test', code => sub { })) }, undef, 'Hook', ); is(exception { ReadableFilePath->('t') }, undef, 'ReadableFilePath'); like( exception { ReadableFilePath->('nosuchdirectory') }, qr/Value "nosuchdirectory" did not pass type constraint "ReadableFilePath"/, 'ReadableFilePath' ); error.t100644001751001751 1355613171470520 14525 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; use Dancer2::Core::App; use Dancer2::Core::Response; use Dancer2::Core::Request; use Dancer2::Core::Error; use JSON::MaybeXS qw/JSON/; # Error serialization my $env = { 'psgi.url_scheme' => 'http', REQUEST_METHOD => 'GET', SCRIPT_NAME => '/foo', PATH_INFO => '/bar/baz', REQUEST_URI => '/foo/bar/baz', QUERY_STRING => 'foo=42&bar=12&bar=13&bar=14', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', REMOTE_ADDR => '127.0.0.1', HTTP_COOKIE => 'dancer.session=1234; fbs_102="access_token=xxxxxxxxxx%7Cffffff"', HTTP_X_FORWARDED_FOR => '127.0.0.2', REMOTE_HOST => 'localhost', HTTP_USER_AGENT => 'Mozilla', REMOTE_USER => 'sukria', }; my $app = Dancer2::Core::App->new( name => 'main' ); my $request = $app->build_request($env); $app->set_request($request); subtest 'basic defaults of Error object' => sub { my $err = Dancer2::Core::Error->new( app => $app ); is $err->status, 500, 'code'; is $err->title, 'Error 500 - Internal Server Error', 'title'; is $err->message, '', 'message'; like $err->content, qr!http://localhost:5000/foo/css!, "error content contains css path relative to uri_base"; }; subtest "send_error in route" => sub { { package App; use Dancer2; set serializer => 'JSON'; get '/error' => sub { send_error "This is a custom error message"; return "send_error returns so this content is not processed"; }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $r = $cb->( GET '/error' ); is( $r->code, 500, 'send_error sets the status to 500' ); like( $r->content, qr{This is a custom error message}, 'Error message looks good', ); is( $r->content_type, 'application/json', 'Response has appropriate content type after serialization', ); }; }; subtest "send_error with custom stuff" => sub { { package App; use Dancer2; get '/error/:x' => sub { my $x = param('x'); send_error "Error $x", "5$x"; }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $r = $cb->( GET '/error/42' ); is( $r->code, 542, 'send_error sets the status to 542' ); like( $r->content, qr{Error 42}, 'Error message looks good' ); }; }; subtest 'Response->error()' => sub { my $resp = Dancer2::Core::Response->new; isa_ok $resp->error( message => 'oops', status => 418 ), 'Dancer2::Core::Error'; is $resp->status => 418, 'response code is 418'; like $resp->content => qr/oops/, 'response content overriden by error'; like $resp->content => qr/teapot/, 'error code title is present'; ok $resp->is_halted, 'response is halted'; }; subtest 'Throwing an error with a response' => sub { my $resp = Dancer2::Core::Response->new; my $err = eval { Dancer2::Core::Error->new( exception => 'our exception', show_errors => 1 )->throw($resp) }; isa_ok($err, 'Dancer2::Core::Response', "Error->throw() accepts a response"); }; subtest 'Error with show_errors: 0' => sub { my $err = Dancer2::Core::Error->new( exception => 'our exception', show_errors => 0 )->throw; unlike $err->content => qr/our exception/; }; subtest 'Error with show_errors: 1' => sub { my $err = Dancer2::Core::Error->new( exception => 'our exception', show_errors => 1 )->throw; like $err->content => qr/our exception/; }; subtest 'App dies with serialized error' => sub { { package AppDies; use Dancer2; set serializer => 'JSON'; get '/die' => sub { die "oh no\n"; # I should serialize }; } my $app = AppDies->to_app; isa_ok( $app, 'CODE', 'Got app' ); test_psgi $app, sub { my $cb = shift; my $r = $cb->( GET '/die' ); is( $r->code, 500, '/die returns 500' ); my $out = eval { JSON->new->utf8(0)->decode($r->decoded_content) }; ok(!$@, 'JSON decoding serializer error produces no errors'); isa_ok($out, 'HASH', 'Error deserializes to a hash'); like($out->{exception}, qr/^oh no/, 'Get expected error message'); }; }; subtest 'Error with exception object' => sub { local $@; eval { MyTestException->throw('a test exception object') }; my $err = Dancer2::Core::Error->new( exception => $@, show_errors => 1, )->throw; like $err->content, qr/a test exception object/, 'Error content contains exception message'; }; subtest 'Errors without server tokens' => sub { { package AppNoServerTokens; use Dancer2; set serializer => 'JSON'; set no_server_tokens => 1; get '/ohno' => sub { die "oh no"; }; } my $test = Plack::Test->create( AppNoServerTokens->to_app ); my $r = $test->request( GET '/ohno' ); is( $r->code, 500, "/ohno returned 500 response"); is( $r->header('server'), undef, "No server header when no_server_tokens => 1" ); }; done_testing; { # Simple test exception class package MyTestException; use overload '""' => \&as_str; sub new { return bless {}; } sub throw { my ( $class, $error ) = @_; my $self = ref($class) ? $class : $class->new; $self->{error} = $error; die $self; } sub as_str { return $_[0]->{error} } } META.json100644001751001751 2774613171470520 14373 0ustar00jasonjason000000000000Dancer2-0.205002{ "abstract" : "Lightweight yet powerful web application framework", "author" : [ "Dancer Core Developers" ], "dynamic_config" : 0, "generated_by" : "Dist::Zilla version 6.009, CPAN::Meta::Converter version 2.150005", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Dancer2", "prereqs" : { "build" : { "requires" : { "ExtUtils::MakeMaker" : "7.1101" } }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "7.1101", "File::ShareDir::Install" : "0.06" } }, "develop" : { "requires" : { "AnyEvent" : "0", "CBOR::XS" : "0", "Class::Method::Modifiers" : "0", "Dist::Zilla::Plugin::Test::UnusedVars" : "0", "Perl::Tidy" : "0", "Test::CPAN::Meta" : "0", "Test::Memory::Cycle" : "0", "Test::MockTime" : "0", "Test::More" : "0.88", "Test::NoTabs" : "0", "Test::Perl::Critic" : "0", "Test::Pod" : "1.41", "Test::Whitespaces" : "0", "YAML::XS" : "0" } }, "runtime" : { "conflicts" : { "YAML" : "1.16" }, "recommends" : { "CGI::Deurl::XS" : "0", "Class::XSAccessor" : "0", "Cpanel::JSON::XS" : "0", "Crypt::URandom" : "0", "HTTP::XSCookies" : "0.000007", "HTTP::XSHeaders" : "0", "Math::Random::ISAAC::XS" : "0", "MooX::TypeTiny" : "0", "Pod::Simple::Search" : "0", "Pod::Simple::SimpleTree" : "0", "Scope::Upper" : "0", "Type::Tiny::XS" : "0", "URL::Encode::XS" : "0", "YAML::XS" : "0" }, "requires" : { "App::Cmd::Setup" : "0", "Attribute::Handlers" : "0", "Carp" : "0", "Config::Any" : "0", "Digest::SHA" : "0", "Encode" : "0", "Exporter" : "5.57", "Exporter::Tiny" : "0", "File::Basename" : "0", "File::Copy" : "0", "File::Find" : "0", "File::Path" : "0", "File::ShareDir" : "0", "File::Spec" : "0", "File::Temp" : "0", "HTTP::Body" : "0", "HTTP::Date" : "0", "HTTP::Headers::Fast" : "0", "HTTP::Tiny" : "0", "HTTP::XSCookies" : "0.000007", "Hash::Merge::Simple" : "0", "Hash::MultiValue" : "0", "Import::Into" : "0", "JSON::MaybeXS" : "0", "List::Util" : "1.29", "MIME::Base64" : "3.13", "Module::Runtime" : "0", "Moo" : "2.000000", "Moo::Role" : "0", "POSIX" : "0", "Plack" : "1.0035", "Plack::Middleware::FixMissingBodyInRedirect" : "0", "Plack::Middleware::RemoveRedundantBody" : "0", "Ref::Util" : "0", "Return::MultiLevel" : "0", "Role::Tiny" : "2.000000", "Safe::Isa" : "0", "Sub::Quote" : "0", "Template" : "0", "Template::Tiny" : "0", "Test::Builder" : "0", "Test::More" : "0.92", "Type::Tiny" : "1.000006", "URI::Escape" : "0", "YAML" : "0.86", "parent" : "0" }, "suggests" : { "Fcntl" : "0", "MIME::Types" : "0" } }, "test" : { "recommends" : { "CPAN::Meta" : "2.120900" }, "requires" : { "Capture::Tiny" : "0.12", "ExtUtils::MakeMaker" : "7.1101", "File::Spec" : "0", "HTTP::Body" : "0", "HTTP::Cookies" : "0", "HTTP::Headers" : "0", "IO::Handle" : "0", "IPC::Open3" : "0", "Template" : "0", "Test::Builder" : "0", "Test::EOL" : "0", "Test::Fatal" : "0", "Test::More" : "0.92" } } }, "provides" : { "Dancer2" : { "file" : "lib/Dancer2.pm", "version" : "0.205002" }, "Dancer2::CLI" : { "file" : "lib/Dancer2/CLI.pm", "version" : "0.205002" }, "Dancer2::CLI::Command::gen" : { "file" : "lib/Dancer2/CLI/Command/gen.pm", "version" : "0.205002" }, "Dancer2::CLI::Command::version" : { "file" : "lib/Dancer2/CLI/Command/version.pm", "version" : "0.205002" }, "Dancer2::Core" : { "file" : "lib/Dancer2/Core.pm", "version" : "0.205002" }, "Dancer2::Core::App" : { "file" : "lib/Dancer2/Core/App.pm", "version" : "0.205002" }, "Dancer2::Core::Cookie" : { "file" : "lib/Dancer2/Core/Cookie.pm", "version" : "0.205002" }, "Dancer2::Core::DSL" : { "file" : "lib/Dancer2/Core/DSL.pm", "version" : "0.205002" }, "Dancer2::Core::Dispatcher" : { "file" : "lib/Dancer2/Core/Dispatcher.pm", "version" : "0.205002" }, "Dancer2::Core::Error" : { "file" : "lib/Dancer2/Core/Error.pm", "version" : "0.205002" }, "Dancer2::Core::Factory" : { "file" : "lib/Dancer2/Core/Factory.pm", "version" : "0.205002" }, "Dancer2::Core::HTTP" : { "file" : "lib/Dancer2/Core/HTTP.pm", "version" : "0.205002" }, "Dancer2::Core::Hook" : { "file" : "lib/Dancer2/Core/Hook.pm", "version" : "0.205002" }, "Dancer2::Core::MIME" : { "file" : "lib/Dancer2/Core/MIME.pm", "version" : "0.205002" }, "Dancer2::Core::Request" : { "file" : "lib/Dancer2/Core/Request.pm", "version" : "0.205002" }, "Dancer2::Core::Request::Upload" : { "file" : "lib/Dancer2/Core/Request/Upload.pm", "version" : "0.205002" }, "Dancer2::Core::Response" : { "file" : "lib/Dancer2/Core/Response.pm", "version" : "0.205002" }, "Dancer2::Core::Response::Delayed" : { "file" : "lib/Dancer2/Core/Response/Delayed.pm", "version" : "0.205002" }, "Dancer2::Core::Role::ConfigReader" : { "file" : "lib/Dancer2/Core/Role/ConfigReader.pm", "version" : "0.205002" }, "Dancer2::Core::Role::DSL" : { "file" : "lib/Dancer2/Core/Role/DSL.pm", "version" : "0.205002" }, "Dancer2::Core::Role::Engine" : { "file" : "lib/Dancer2/Core/Role/Engine.pm", "version" : "0.205002" }, "Dancer2::Core::Role::Handler" : { "file" : "lib/Dancer2/Core/Role/Handler.pm", "version" : "0.205002" }, "Dancer2::Core::Role::HasLocation" : { "file" : "lib/Dancer2/Core/Role/HasLocation.pm", "version" : "0.205002" }, "Dancer2::Core::Role::Hookable" : { "file" : "lib/Dancer2/Core/Role/Hookable.pm", "version" : "0.205002" }, "Dancer2::Core::Role::Logger" : { "file" : "lib/Dancer2/Core/Role/Logger.pm", "version" : "0.205002" }, "Dancer2::Core::Role::Serializer" : { "file" : "lib/Dancer2/Core/Role/Serializer.pm", "version" : "0.205002" }, "Dancer2::Core::Role::SessionFactory" : { "file" : "lib/Dancer2/Core/Role/SessionFactory.pm", "version" : "0.205002" }, "Dancer2::Core::Role::SessionFactory::File" : { "file" : "lib/Dancer2/Core/Role/SessionFactory/File.pm", "version" : "0.205002" }, "Dancer2::Core::Role::StandardResponses" : { "file" : "lib/Dancer2/Core/Role/StandardResponses.pm", "version" : "0.205002" }, "Dancer2::Core::Role::Template" : { "file" : "lib/Dancer2/Core/Role/Template.pm", "version" : "0.205002" }, "Dancer2::Core::Route" : { "file" : "lib/Dancer2/Core/Route.pm", "version" : "0.205002" }, "Dancer2::Core::Runner" : { "file" : "lib/Dancer2/Core/Runner.pm", "version" : "0.205002" }, "Dancer2::Core::Session" : { "file" : "lib/Dancer2/Core/Session.pm", "version" : "0.205002" }, "Dancer2::Core::Time" : { "file" : "lib/Dancer2/Core/Time.pm", "version" : "0.205002" }, "Dancer2::Core::Types" : { "file" : "lib/Dancer2/Core/Types.pm", "version" : "0.205002" }, "Dancer2::FileUtils" : { "file" : "lib/Dancer2/FileUtils.pm", "version" : "0.205002" }, "Dancer2::Handler::AutoPage" : { "file" : "lib/Dancer2/Handler/AutoPage.pm", "version" : "0.205002" }, "Dancer2::Handler::File" : { "file" : "lib/Dancer2/Handler/File.pm", "version" : "0.205002" }, "Dancer2::Logger::Capture" : { "file" : "lib/Dancer2/Logger/Capture.pm", "version" : "0.205002" }, "Dancer2::Logger::Capture::Trap" : { "file" : "lib/Dancer2/Logger/Capture/Trap.pm", "version" : "0.205002" }, "Dancer2::Logger::Console" : { "file" : "lib/Dancer2/Logger/Console.pm", "version" : "0.205002" }, "Dancer2::Logger::Diag" : { "file" : "lib/Dancer2/Logger/Diag.pm", "version" : "0.205002" }, "Dancer2::Logger::File" : { "file" : "lib/Dancer2/Logger/File.pm", "version" : "0.205002" }, "Dancer2::Logger::Note" : { "file" : "lib/Dancer2/Logger/Note.pm", "version" : "0.205002" }, "Dancer2::Logger::Null" : { "file" : "lib/Dancer2/Logger/Null.pm", "version" : "0.205002" }, "Dancer2::Plugin" : { "file" : "lib/Dancer2/Plugin.pm", "version" : "0.205002" }, "Dancer2::Serializer::Dumper" : { "file" : "lib/Dancer2/Serializer/Dumper.pm", "version" : "0.205002" }, "Dancer2::Serializer::JSON" : { "file" : "lib/Dancer2/Serializer/JSON.pm", "version" : "0.205002" }, "Dancer2::Serializer::Mutable" : { "file" : "lib/Dancer2/Serializer/Mutable.pm", "version" : "0.205002" }, "Dancer2::Serializer::YAML" : { "file" : "lib/Dancer2/Serializer/YAML.pm", "version" : "0.205002" }, "Dancer2::Session::Simple" : { "file" : "lib/Dancer2/Session/Simple.pm", "version" : "0.205002" }, "Dancer2::Session::YAML" : { "file" : "lib/Dancer2/Session/YAML.pm", "version" : "0.205002" }, "Dancer2::Template::Implementation::ForkedTiny" : { "file" : "lib/Dancer2/Template/Implementation/ForkedTiny.pm", "version" : "0.205002" }, "Dancer2::Template::Simple" : { "file" : "lib/Dancer2/Template/Simple.pm", "version" : "0.205002" }, "Dancer2::Template::TemplateToolkit" : { "file" : "lib/Dancer2/Template/TemplateToolkit.pm", "version" : "0.205002" }, "Dancer2::Template::Tiny" : { "file" : "lib/Dancer2/Template/Tiny.pm", "version" : "0.205002" }, "Dancer2::Test" : { "file" : "lib/Dancer2/Test.pm", "version" : "0.205002" } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/PerlDancer/Dancer2/issues" }, "homepage" : "http://perldancer.org/", "repository" : { "type" : "git", "url" : "git://github.com/PerlDancer/Dancer2.git", "web" : "https://github.com/PerlDancer/Dancer2" }, "x_IRC" : "irc://irc.perl.org/#dancer", "x_WebIRC" : "https://chat.mibbit.com/#dancer@irc.perl.org" }, "version" : "0.205002", "x_serialization_backend" : "Cpanel::JSON::XS version 3.0233" } caller.t100644001751001751 76313171470520 14572 0ustar00jasonjason000000000000Dancer2-0.205002/t#!perl use strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; use File::Spec; { package App; use Dancer2; get '/' => sub { app->caller }; } my $app = App->to_app; test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/' ); is( $res->code, 200, '[GET /] Successful' ); is( File::Spec->canonpath( $res->content), File::Spec->catfile(t => 'caller.t'), 'Correct App name from caller', ); }; cookie.t100644001751001751 1504113171470520 14634 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::Fatal; use Test::More; BEGIN { # Freeze time at Tue, 15-Jun-2010 00:00:00 GMT *CORE::GLOBAL::time = sub { return 1276560000 } } use Dancer2::Core::Cookie; use Dancer2::Core::Request; diag "If you want extra speed, install HTTP::XSCookies" if !Dancer2::Core::Cookie::_USE_XS; sub run_test { note "Constructor"; my $cookie = Dancer2::Core::Cookie->new( name => "foo" ); isa_ok $cookie => 'Dancer2::Core::Cookie'; can_ok $cookie => 'to_header'; note "Setting values"; is $cookie->value("foo") => "foo", "Can set value"; is $cookie->value => "foo", "Set value stuck"; is $cookie . "bar", "foobar", "Stringifies to desired value"; ok $cookie->value( [qw(a b c)] ), "can set multiple values"; is $cookie->value => 'a', "get first value in scalar context"; is_deeply [ $cookie->value ] => [qw(a b c)], "get all values in list context";; ok $cookie->value( { x => 1, y => 2 } ), "can set values with a hashref"; like $cookie->value => qr/^[xy]$/; # hashes doesn't store order... is_deeply [ sort $cookie->value ] => [ sort ( 1, 2, 'x', 'y' ) ]; note "accessors and defaults"; is $cookie->name => 'foo', "name is as expected"; is $cookie->name("bar") => "bar", "can change name"; is $cookie->name => 'bar', "name change stuck"; ok !$cookie->domain, "no domain set by default"; is $cookie->domain("dancer.org") => "dancer.org", "setting domain returns new value"; is $cookie->domain => "dancer.org", "new domain valjue stuck"; is $cookie->domain("") => "", "can clear domain"; ok !$cookie->domain, "no domain set now"; is $cookie->path => '/', "by default, path is /"; ok $cookie->has_path, "has_path"; is $cookie->path("/foo") => "/foo", "setting path returns new value"; ok $cookie->has_path, "has_path"; is $cookie->path => "/foo", "new path stuck"; ok !$cookie->secure, "no cookie secure flag by default"; is $cookie->secure(1) => 1, "enabling \$cookie->secure returns new value"; is $cookie->secure => 1, "\$cookie->secure flag is enabled"; is $cookie->secure(0) => 0, "disabling \$cookie->secure returns new value"; ok !$cookie->secure, "\$cookie->secure flag is disabled"; ok $cookie->http_only, "http_only by default"; is $cookie->http_only(0) => 0, "disabling \$cookie->http_only returns new value"; ok !$cookie->http_only, "\$cookie->http_only is now disabled"; like exception { $cookie->same_site('foo') }, qr/Value "foo" did not pass type constraint "Enum\["Strict","Lax"\]/; note "expiration strings"; my $min = 60; my $hour = 60 * $min; my $day = 24 * $hour; my $week = 7 * $day; my $mon = 30 * $day; my $year = 365 * $day; ok !$cookie->expires; my %times = ( "+2" => "Tue, 15-Jun-2010 00:00:02 GMT", "+2h" => "Tue, 15-Jun-2010 02:00:00 GMT", "-2h" => "Mon, 14-Jun-2010 22:00:00 GMT", "1 hour" => "Tue, 15-Jun-2010 01:00:00 GMT", "3 weeks 4 days 2 hours 99 min 0 secs" => "Sat, 10-Jul-2010 03:39:00 GMT", "2 months" => "Sat, 14-Aug-2010 00:00:00 GMT", "12 years" => "Sun, 12-Jun-2022 00:00:00 GMT", 1288817656 => "Wed, 03-Nov-2010 20:54:16 GMT", 1288731256 => "Tue, 02-Nov-2010 20:54:16 GMT", 1288644856 => "Mon, 01-Nov-2010 20:54:16 GMT", 1288558456 => "Sun, 31-Oct-2010 20:54:16 GMT", 1288472056 => "Sat, 30-Oct-2010 20:54:16 GMT", 1288385656 => "Fri, 29-Oct-2010 20:54:16 GMT", 1288299256 => "Thu, 28-Oct-2010 20:54:16 GMT", 1288212856 => "Wed, 27-Oct-2010 20:54:16 GMT", # Anything not understood is passed through "basset hounds got long ears" => "basset hounds got long ears", "+2 something" => "+2 something", ); for my $exp ( keys %times ) { my $want = $times{$exp}; $cookie->expires($exp); is $cookie->expires => $want, "expiry $exp => $want";; } note "to header"; my @cake = ( { cookie => { name => 'bar', value => 'foo', expires => '+2h', secure => 1 }, expected => sprintf( "bar=foo; Expires=%s; HttpOnly; Path=/; Secure", $times{'+2h'}, ), }, { cookie => { name => 'bar', value => 'foo', domain => 'dancer.org', path => '/dance', http_only => 1 }, expected => "bar=foo; Domain=dancer.org; HttpOnly; Path=/dance", }, { cookie => { name => 'bar', value => 'foo', }, expected => "bar=foo; HttpOnly; Path=/", }, { cookie => { name => 'bar', value => 'foo', http_only => 0, }, expected => "bar=foo; Path=/", }, { cookie => { name => 'same-site', value => 'strict', same_site => 'Strict', }, expected => 'same-site=strict; HttpOnly; Path=/; SameSite=Strict', }, { cookie => { name => 'same-site', value => 'lax', same_site => 'Lax', }, expected => 'same-site=lax; HttpOnly; Path=/; SameSite=Lax', }, ); for my $cook (@cake) { my $c = Dancer2::Core::Cookie->new(%{$cook->{cookie}}); # name=value; sorted fields my @a = split /; /, $c->to_header; is join("; ", shift @a, sort @a), $cook->{expected}; } note 'multi-value'; my $c = Dancer2::Core::Cookie->new( name => 'foo', value => [qw/bar baz/] ); is $c->to_header, 'foo=bar&baz; Path=/; HttpOnly'; my $r = Dancer2::Core::Request->new( env => { HTTP_COOKIE => 'foo=bar&baz' } ); is_deeply [ $r->cookies->{foo}->value ], [qw/bar baz/]; } note "Run test with XS_HTTP_COOKIES" if Dancer2::Core::Cookie::_USE_XS; run_test(); if ( Dancer2::Core::Cookie::_USE_XS ) { note "Run test without XS_HTTP_COOKIES"; no warnings 'redefine'; *Dancer2::Core::Cookie::to_header = \&Dancer2::Core::Cookie::pp_to_header; run_test(); } done_testing; engine.t100644001751001751 263613171470520 14616 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::Core::App; use Dancer2::Template::Tiny; { my $f = Dancer2::Template::Tiny->new(); isa_ok( $f, 'Dancer2::Template::Tiny' ); ok( $f->does('Dancer2::Core::Role::Engine'), 'Consumed Role::Engine', ); ok( $f->does('Dancer2::Core::Role::Template'), 'Consumed Role::Template', ); is( $f->name, 'Tiny', 'Correct engine name' ); } # checks for validity of engine names my $app = Dancer2::Core::App->new(); isa_ok( $app, 'Dancer2::Core::App' ); { no warnings qw; *Dancer2::Core::Factory::create = sub { $_[1] }; } foreach my $engine_type ( qw ) { note($engine_type); my $engine; my $build_method = "_build_${engine_type}_engine"; is( exception { $engine = $app->$build_method( undef, { $engine_type => 'Fake43Thing' } ); }, undef, "Built $engine_type successfully with proper name", ); like( exception { $engine = $app->$build_method( undef, { $engine_type => '7&&afail' } ); }, qr/^Cannot load $engine_type engine '7&&afail': illegal module name/, "Failed creating $engine_type with illegal name", ); is( $engine, $engine_type, 'Correct response from override' ); } done_testing; logger.t100644001751001751 533313171470520 14625 0ustar00jasonjason000000000000Dancer2-0.205002/tuse Test::More; use strict; use warnings; BEGIN { # Freeze time at Tue, 15-Jun-2010 00:00:00 GMT *CORE::GLOBAL::time = sub { return 1276560000 } } my $_logs = []; { package Dancer2::Logger::Test; use Moo; with 'Dancer2::Core::Role::Logger'; sub log { my ( $self, $level, $message ) = @_; push @$_logs, $self->format_message( $level, $message ); } } my $logger = Dancer2::Logger::Test->new( app_name => 'test' ); is $logger->log_level, 'debug'; $logger->debug("foo"); # Hard to make caller(6) work when we deal with the logger directly, # so do not check for a specific filename. like $_logs->[0], qr{debug \@2010-06-1\d \d\d:\d\d:00> foo in }; subtest 'log level and capture' => sub { use Dancer2::Logger::Capture; use Dancer2; # NOTE: this will read the config.yml under t/ that defines log level as info set logger => 'capture'; warning "Danger! Warning!"; info "Tango, Foxtrot"; debug "I like pie."; my $trap = dancer_app->engine('logger')->trapper; my $msg = $trap->read; delete $msg->[0]{'formatted'}; delete $msg->[1]{'formatted'}; is_deeply $msg, [ { level => "warning", message => "Danger! Warning!", }, { level => "info", message => "Tango, Foxtrot", }, ]; # each call to read cleans the trap is_deeply $trap->read, []; }; subtest 'logger enging hooks' => sub { # before hook can change log level or message. hook 'engine.logger.before' => sub { my $logger = shift; # @_ = ( $level, @message_args ) $_[0] = 'panic'; # eg. log all messages at the 'panic' level }; my $str = "Thou shalt not pass"; warning $str; my $trap = dancer_app->engine('logger')->trapper; my $msg = $trap->read; delete $msg->[0]{'formatted'}; is_deeply $msg, [ { level => "panic", message => $str, }, ]; }; subtest 'logger file' => sub { use Dancer2; use File::Temp qw/tempdir/; my $dir = tempdir( CLEANUP => 1 ); set engines => { logger => { File => { log_dir => $dir, file_name => 'test', } } }; # XXX this sucks, we need to set the engine *before* the logger # - Franck, 2013/08/03 set logger => 'file'; warning "Danger! Warning!"; open my $log_file, '<', File::Spec->catfile($dir, 'test'); my $txt = <$log_file>; like $txt, qr/Danger! Warning!/; }; # Explicitly close the logger file handle for those systems that # do not allow "open" files to be unlinked (Windows). GH#424. my $log_engine = engine('logger'); close $log_engine->fh; done_testing; GitGuide.md100644001751001751 2167613171470520 14771 0ustar00jasonjason000000000000Dancer2-0.205002# Git Guide This guide will help you to set up your environment to be able to work on the Dancer2's repository. ## Contributing This guide has been written to help anyone interested in contributing to the development of Dancer2. First of all - thank you for your interest in the project! It's the community of helpful contributors who've helped Dancer grow phenomenally. Without the community we wouldn't be where we are today! Please read this guide before contributing to Dancer2, to avoid wasted effort and maximizing the chances of your contributions being used. There are many ways to contribute to the project. Dancer2 is a young yet active project and any kind of help is very much appreciated! ### Documentation We value documentation very much, but it's difficult to keep it up-to-date. If you find a typo or an error in the documentation please do let us know - ideally by submitting a patch (pull request) with your fix or suggestion (see [Patch Submission](#environment-and-patch-submission)). ### Code You can write extensions (plugins) for Dancer2 extending core functionality or contribute to Dancer2's core code, see [Patch Submission](#environment-and-patch-submission) below. ## General Development Guidelines This section lists high-level recommendations for developing Dancer2, for more detailed guidelines, see [Coding Guidelines](#coding-guidelines) below. ### Quality Assurance Dancer2 should be able to install for all Perl versions since 5.8, on any platform for which Perl exists. We focus mainly on GNU/Linux (any distribution), \*BSD and Windows (native and Cygwin). We should avoid regressions as much as possible and keep backwards compatibility in mind when refactoring. Stable releases should not break functionality and new releases should provide an upgrade path and upgrade tips such as warning the user about deprecated functionality. ### Quality Supervision We can measure our quality using the [CPAN testers platform](http://www.cpantesters.org). A good way to help the project is to find a failing build log on the [CPAN testers](http://www.cpantesters.org/distro/D/Dancer2.html). If you find a failing test report, feel free to report it as a [GitHub issue](http://github.com/PerlDancer/Dancer2/issues). ### Reporting Bugs We prefer to have all our bug reports on GitHub, in the [issues section](http://github.com/PerlDancer/Dancer2/issues). Please make sure the bug you're reporting does not yet exist. If in doubt please ask on IRC. ## Environment and Patch Submission ### Set up a development environment If you want to submit a patch for Dancer2, you need git and very likely also [_Dist::Zilla_](https://metacpan.org/module/Dist::Zilla). We also recommend perlbrew (see below) or, alternatively, [_App::Plenv_](https://github.com/tokuhirom/plenv)) to test and develop Dancer2 on a recent version of Perl. We also suggest [_App::cpanminus_](https://metacpan.org/module/App::cpanminus) to quickly and comfortably install Perl modules. In the following sections we provide tips for the installation of some of these tools together with Dancer. Please also see the documentation that comes with these tools for more info. #### Perlbrew tips (Optional) Install perlbrew for example with $ cpanm App::perlbrew Check which Perls are available $ perlbrew available It should list the available Perl versions, like this (incomplete) list: perl-5.17.1 perl-5.16.0 perl-5.14.2 perl-5.12.4 ... Now run the init command for perlbrew. The init command initializes and controls processes. The init command is run as the last step of any startup process. $ perlbrew init Then install a version inside perlbrew. We recommend you give a name to the installation (`--as` option), as well as compiling without the tests (`--n` option) to speed it up. $ perlbrew install -n perl-5.14.4 --as dancer_development -j 3 Wait a while, and it should be done. Switch to your new Perl with: $ perlbrew switch dancer_development Now you are using the fresh Perl, you can check it with: $ which perl Install cpanm on your brewed version of Perl. $ perlbrew install-cpanm ### Install various dependencies (required) Install Dist::Zilla $ cpanm Dist::Zilla ### Get Dancer2 sources Get the Dancer sources from github (for a more complete git workflow see below): Clone your fork to have a local copy using the following command: $ git clone git://github.com/perldancer/Dancer2.git The Dancer2 sources come with a `dist.ini`. That's the configuration file for _Dist::Zilla_, so that it knows how to build Dancer2. Let's use dist.ini to install additional `Dist::Zilla` plugins which are not yet installed on your system (or Perl installation): $ dzil authordeps | cpanm -n That should install a bunch of stuff. Now that _Dist::Zilla_ is up and running, you should install the dependencies required by Dancer2: $ dzil listdeps | cpanm -n When that is done, you're good to go! You can use `dzil` to build and test Dancer2: $ dzil build $ dzil test --no-author ### Running your modified version If you have any version of Dancer2 installed on your system you will likely run into problems when you try and run the "Dancer2" binary due to the wrong lib's being used. The following command should resolve that. ```bash perl -Ilib script/dancer2 gen -s share/skel --overwrite --path /tmp/d2app -a MyApp::App ``` - It assumes we are in the git repo root dir - `-Ilib` - tells perl to include the lib dir in it's search path - in this case we run "gen" and - `-s share/skel` - specify the use of the local copy of the skel dir - `--overwrite` - we want to overwrite the generated scaffold project - `--path /tmp/d2app` - the dir to write the generated scaffold project dir to - `-a MyApp::App` - the name of the app project we want to generate ### Patch Submission (Github workflow) The Dancer2 development team uses GitHub to collaborate. We greatly appreciate contributions submitted via GitHub, as it makes tracking these contributions and applying them much, much easier. This gives your contribution a much better chance of being integrated into Dancer2 quickly! **NOTE:** All active development is performed in the _master_ branch. Therefore, all your contribution work should be done in a fork of the _master_ branch. Here is the workflow for submitting a patch: 1. Fork the repository: http://github.com/PerlDancer/Dancer2 and click "Fork"; 2. Clone your fork to have a local copy using the following command: $ git clone git://github.com/myname/Dancer2.git 3. As a contributor, you should **always** work on the `master` branch of your clone. $ git remote add upstream https://github.com/PerlDancer/Dancer2.git $ git fetch upstream This will create a local branch in your clone named _master_ and that will track the official _master_ branch. That way, if you have more or less commits than the upstream repo, you'll be immediately notified by git. 4. You want to isolate all your commits in a _topic_ branch, this will make the reviewing much easier for the core team and will allow you to continue working on your clone without worrying about different commits mixing together. To do that, first create a local branch to build your pull request: # you should be in master here $ git checkout -b pr/$name Now you have created a local branch named _pr/$name_ where _$name_ is the name you want (it should describe the purpose of the pull request you're preparing). In that branch, do all the commits you need (the more the better) and when done, push the branch to your fork: # ... commits ... git push origin pr/$name You are now ready to send a pull request. 5. Send a _pull request_ via the GitHub interface. Make sure your pull request is based on the _pr/$name_ branch you've just pushed, so that it incorporates the appropriate commits only. It's also a good idea to summarize your work in a report sent to the users' mailing list (see below), in order to make sure the team is aware of it. You could also notify the core team on IRC, on `irc.perl.org`, channel `#dancer` or via [web client](http://www.perldancer.org/irc). 6. When the core team reviews your pull request, it will either accept (and then merge into _master_) or refuse your request. If it's refused, try to understand the reasons explained by the team for the denial. Most of the time, communicating with the core team is enough to understand what the mistake was. Above all, please don't be offended. If your pull request is merged into _master_, then all you have to do is remove your local and remote _pr/$name_ branch: $ git checkout master $ git branch -D pr/$name $ git push origin :pr/$name And then, of course, you need to sync your local devel branch with upstream: $ git pull upstream master $ git push origin master You're now ready to start working on a new pull request! request.t100644001751001751 2216513171470520 15060 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Dancer2::Core::App; use Dancer2::Core::Request; diag "If you want extra speed, install URL::Encode::XS" if !$Dancer2::Core::Request::XS_URL_DECODE; diag "If you want extra speed, install CGI::Deurl::XS" if !$Dancer2::Core::Request::XS_PARSE_QUERY_STRING; sub run_test { my $env = { 'psgi.url_scheme' => 'http', REQUEST_METHOD => 'GET', SCRIPT_NAME => '/foo', PATH_INFO => '/bar/baz', REQUEST_URI => '/foo/bar/baz', QUERY_STRING => 'foo=42&bar=12&bar=13&bar=14', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', REMOTE_ADDR => '127.0.0.1', HTTP_X_FORWARDED_FOR => '127.0.0.2', HTTP_X_FORWARDED_HOST => 'secure.frontend', HTTP_X_FORWARDED_PROTOCOL => 'https', REMOTE_HOST => 'localhost', HTTP_USER_AGENT => 'Mozilla', REMOTE_USER => 'sukria', HTTP_COOKIE => 'cookie.a=foo=bar; cookie.b=1234abcd; no.value.cookie', }; my $req = Dancer2::Core::Request->new( env => $env ); note "tests for accessors"; is $req->agent, 'Mozilla'; is $req->user_agent, 'Mozilla'; is $req->remote_address, '127.0.0.1'; is $req->address, '127.0.0.1'; is $req->forwarded_for_address, '127.0.0.2'; is $req->remote_host, 'localhost'; is $req->protocol, 'HTTP/1.1'; is $req->port, 5000; is $req->request_uri, '/foo/bar/baz'; is $req->uri, '/foo/bar/baz'; is $req->user, 'sukria'; is $req->script_name, '/foo'; is $req->scheme, 'http'; is $req->referer, undef; ok( !$req->secure ); is $req->method, 'GET'; is $req->request_method, 'GET'; ok( $req->is_get ); ok( !$req->is_post ); ok( !$req->is_put ); ok( !$req->is_delete ); ok( !$req->is_patch ); ok( !$req->is_head ); is $req->id, 1; is $req->to_string, '[#1] GET /bar/baz'; note "tests params"; is_deeply { $req->params }, { foo => 42, bar => [ 12, 13, 14 ] }; note "tests cookies"; is( keys %{ $req->cookies }, 2, "multiple cookies extracted" ); my $forward = Dancer2::Core::App->new( request => $req ) ->make_forward_to('/somewhere'); is $forward->path_info, '/somewhere'; is $forward->method, 'GET'; note "tests for uri_for"; is $req->base, 'http://localhost:5000/foo'; is $req->uri_for( 'bar', { baz => 'baz' } ), 'http://localhost:5000/foo/bar?baz=baz'; is $req->uri_for('/bar'), 'http://localhost:5000/foo/bar'; is $req->uri_for( '/bar', undef, 1 ), 'http://localhost:5000/foo/bar', 'uri_for returns a URI (with $dont_escape)'; is $req->request_uri, '/foo/bar/baz'; is $req->path_info, '/bar/baz'; { local $env->{SCRIPT_NAME} = ''; is $req->uri_for('/foo'), 'http://localhost:5000/foo'; } { local $env->{SERVER_NAME} = 0; is $req->base, 'http://0:5000/foo'; local $env->{HTTP_HOST} = 'oddhostname:5000'; is $req->base, 'http://oddhostname:5000/foo'; } note "testing behind proxy"; { my $req = Dancer2::Core::Request->new( env => $env, is_behind_proxy => 1 ); is $req->secure, 1; is $req->host, $env->{HTTP_X_FORWARDED_HOST}; is $req->scheme, 'https'; } note "testing behind proxy when optional headers are not set"; { # local modifications to env: local $env->{HTTP_HOST} = 'oddhostname:5000'; delete local $env->{'HTTP_X_FORWARDED_FOR'}; delete local $env->{'HTTP_X_FORWARDED_HOST'}; delete local $env->{'HTTP_X_FORWARDED_PROTOCOL'}; my $req = Dancer2::Core::Request->new( env => $env, is_behind_proxy => 1 ); is ! $req->secure, 1; is $req->host, 'oddhostname:5000'; is $req->scheme, 'http'; } note "testing path and uri_base"; { # Base env used for path and uri_base tests my $base = { 'psgi.url_scheme' => 'http', REQUEST_METHOD => 'GET', QUERY_STRING => '', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', }; # PATH_INFO not set my $env = { %$base, SCRIPT_NAME => '/foo', PATH_INFO => '', REQUEST_URI => '/foo', }; my $req = Dancer2::Core::Request->new( env => $env ); is( $req->path, '/', 'path corrent when empty PATH_INFO' ); is( $req->uri_base, 'http://localhost:5000/foo', 'uri_base correct when empty PATH_INFO' ); # SCRIPT_NAME not set $env = { %$base, SCRIPT_NAME => '', PATH_INFO => '/foo', REQUEST_URI => '/foo', }; $req = Dancer2::Core::Request->new( env => $env ); is( $req->path, '/foo', 'path corrent when empty SCRIPT_NAME' ); is( $req->uri_base, 'http://localhost:5000', 'uri_base handles empty SCRIPT_NAME' ); # Both SCRIPT_NAME and PATH_INFO set # PSGI spec does not allow SCRIPT_NAME='/', PATH_INFO='/some/path' $env = { %$base, SCRIPT_NAME => '/foo', PATH_INFO => '/bar/baz/', REQUEST_URI => '/foo/bar/baz/', }; $req = Dancer2::Core::Request->new( env => $env ); is( $req->path, '/bar/baz/', 'path corrent when both PATH_INFO and SCRIPT_NAME set' ); is( $req->uri_base, 'http://localhost:5000/foo', 'uri_base correct when both PATH_INFO and SCRIPT_NAME set', ); # Neither SCRIPT_NAME or PATH_INFO set $env = { %$base, SCRIPT_NAME => '', PATH_INFO => '', REQUEST_URI => '/foo/', }; $req = Dancer2::Core::Request->new( env => $env ); is( $req->path, '/', 'path corrent when calculated from REQUEST_URI' ); is( $req->uri_base, 'http://localhost:5000', 'uri_base correct when calculated from REQUEST_URI', ); } note "testing forward"; $env = { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/', 'PATH_INFO' => '/', 'QUERY_STRING' => 'foo=bar&number=42', }; $req = Dancer2::Core::Request->new( env => $env ); is $req->path, '/', 'path is /'; is $req->method, 'GET', 'method is get'; is_deeply scalar( $req->params ), { foo => 'bar', number => 42 }, 'params are parsed'; $req = Dancer2::Core::App->new( request => $req ) ->make_forward_to('/new/path'); is $req->path, '/new/path', 'path is changed'; is $req->method, 'GET', 'method is unchanged'; is_deeply scalar( $req->params ), { foo => 'bar', number => 42 }, 'params are not touched'; $req = Dancer2::Core::App->new( request => $req ) ->make_forward_to( '/new/path', undef, { method => 'POST' }, ); is $req->path, '/new/path', 'path is changed'; is $req->method, 'POST', 'method is changed'; is_deeply scalar( $req->params ), { foo => 'bar', number => 42 }, 'params are not touched'; note "testing unicode params"; $env = { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/', 'PATH_INFO' => '/', 'QUERY_STRING' => "M%C3%BCller=L%C3%BCdenscheid", }; $req = Dancer2::Core::Request->new( env => $env ); is_deeply scalar( $req->params ), { "M\N{U+00FC}ller", "L\N{U+00FC}denscheid" }, 'multi byte unicode chars work in param keys and values'; { note "testing private _decode not to mangle hash"; my @warnings; local $SIG{__WARN__} = sub { push @warnings, @_; }; my $h = { zzz => undef, }; for ( 'aaa' .. 'fff' ) { $h->{$_} = $_; } my $i = Dancer2::Core::Request::_decode($h); is_deeply( $i, $h, 'hash not mangled' ); ok( !@warnings, 'no warnings were issued' ); } } note "Run test with XS_URL_DECODE" if $Dancer2::Core::Request::XS_URL_DECODE; note "Run test with XS_PARSE_QUERY_STRING" if $Dancer2::Core::Request::XS_PARSE_QUERY_STRING; run_test(); if ($Dancer2::Core::Request::XS_PARSE_QUERY_STRING) { note "Run test without XS_PARSE_QUERY_STRING"; $Dancer2::Core::Request::XS_PARSE_QUERY_STRING = 0; $Dancer2::Core::Request::_id = 0; run_test(); } if ($Dancer2::Core::Request::XS_URL_DECODE) { note "Run test without XS_URL_DECODE"; $Dancer2::Core::Request::XS_URL_DECODE = 0; $Dancer2::Core::Request::_id = 0; run_test(); } done_testing; uri_for.t100644001751001751 103513171470520 15006 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package App; use Dancer2; get '/foo' => sub { return uri_for('/foo'); }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/foo' )->code, 200, '/foo code okay' ); is( $cb->( GET '/foo' )->content, 'http://localhost/foo', 'uri_for works as expected', ); }; done_testing; forward.t100644001751001751 1046213171470520 15031 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; use Dancer2; set behind_proxy => 1; get '/' => sub { 'home:' . join( ',', params ); }; get '/bounce/' => sub { return forward '/'; }; get '/bounce/:withparams/' => sub { return forward '/'; }; get '/bounce2/adding_params/' => sub { return forward '/', { withparams => 'foo' }; }; post '/simple_post_route/' => sub { 'post:' . join( ',', params ); }; get '/go_to_post/' => sub { return forward '/simple_post_route/', { foo => 'bar' }, { method => 'post' }; }; get '/proxy/' => sub { return uri_for('/'); }; get '/forward_with_proxy/' => sub { forward '/proxy/'; }; # NOT SUPPORTED IN DANCER2 # In dancer2, vars are alive for only one request flow, a forward initiate a # new request flow, then the vars HashRef is destroyed. # # get '/b' => sub { vars->{test} = 1; forward '/a'; }; # get '/a' => sub { return "test is " . var('test'); }; my $app = __PACKAGE__->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/' )->code, 200, '[GET /] Correct code' ); is( $cb->( GET '/' )->content, 'home:', '[GET /] Correct content' ); is( $cb->( GET '/bounce/' )->code, 200, '[GET /bounce] Correct code' ); is( $cb->( GET '/bounce/' )->content, 'home:', '[GET /bounce] Correct content', ); is( $cb->( GET '/bounce/thesethings/' )->code, 200, '[GET /bounce/thesethings/] Correct code', ); is( $cb->( GET '/bounce/thesethings/' )->content, 'home:withparams,thesethings', '[GET /bounce/thesethings/] Correct content', ); is( $cb->( GET '/bounce2/adding_params/' )->code, 200, '[GET /bounce2/adding_params/] Correct code', ); is( $cb->( GET '/bounce2/adding_params/' )->content, 'home:withparams,foo', '[GET /bounce2/adding_params/] Correct content', ); is( $cb->( GET '/go_to_post/' )->code, 200, '[GET /go_to_post/] Correct code', ); is( $cb->( GET '/go_to_post/' )->content, 'post:foo,bar', '[GET /go_to_post/] Correct content', ); # NOT SUPPORTED # response_status_is [ GET => '/b' ] => 200; # response_content_is [ GET => '/b' ] => 'test is 1'; { my $res = $cb->( GET '/bounce/' ); is( $res->headers->content_length, 5, '[GET /bounce/] Correct content length', ); is( $res->headers->content_type, 'text/html', '[GET /bounce/] Correct content type', ); is( $res->headers->content_type_charset, 'UTF-8', '[GET /bounce/] Correct content type charset', ); is( $res->headers->server, "Perl Dancer2 " . Dancer2->VERSION, '[GET /bounce/] Correct Server', ); } # checking post post '/' => sub {'post-home'}; post '/bounce/' => sub { forward('/') }; is( $cb->( POST '/' )->code, 200, '[POST /] Correct code' ); is( $cb->( POST '/' )->content, 'post-home', '[POST /] Correct content' ); is( $cb->( POST '/bounce/' )->code, 200, '[POST /bounce/] Correct code', ); is( $cb->( POST '/bounce/' )->content, 'post-home', '[POST /bounce/] Correct content', ); { my $res = $cb->( POST '/bounce/' ); is( $res->headers->content_length, 9, '[POST /bounce/] Correct content length', ); is( $res->headers->content_type, 'text/html', '[POST /bounce/] Correct content type', ); is( $res->headers->content_type_charset, 'UTF-8', '[POST /bounce/] Correct content type charset', ); is( $res->headers->server, "Perl Dancer2 " . Dancer2->VERSION, '[POST /bounce/] Correct Server', ); } is( $cb->( GET '/forward_with_proxy/', 'X-Forwarded-Proto' => 'https' )->content, 'https://localhost/', '[GET /forward_with_proxy/] maintained is_behind_proxy', ); }; done_testing; factory.t100644001751001751 102013171470520 15002 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::Core; use Dancer2::Core::Factory; is Dancer2::Core::camelize('foo_bar_baz'), 'FooBarBaz'; is Dancer2::Core::camelize('FooBarBaz'), 'FooBarBaz'; like( exception { my $l = Dancer2::Core::Factory->create( unknown => 'stuff' ) }, qr{Unable to load class for Unknown component Stuff:}, 'Failure to load nonexistent class', ); my $l = Dancer2::Core::Factory->create( logger => 'console' ); isa_ok $l, 'Dancer2::Logger::Console'; done_testing; dsl000755001751001751 013171470520 13577 5ustar00jasonjason000000000000Dancer2-0.205002/tany.t100644001751001751 221013171470520 14706 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; any [ 'get', 'post' ] => '/test' => sub { request->method }; any '/all' => sub { request->method }; } my $test = Plack::Test->create( App->to_app ); subtest 'any with params' => sub { my @success = qw; my @fails = qw; foreach my $method (@success) { my $req = HTTP::Request->new( $method => '/test' ); is( $test->request($req)->content, $method, "Method $method works", ); } foreach my $method (@fails) { my $req = HTTP::Request->new( $method => '/test' ); ok( ! $test->request($req)->is_success, "Method $method doesn't exist", ); } }; subtest 'any without params' => sub { foreach my $method ( qw ) { my $req = HTTP::Request->new( $method => '/all' ); is( $test->request($req)->content, $method, "Method $method works", ); } }; app.t100644001751001751 53413171470520 14666 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { my $app = app; ::isa_ok( $app, 'Dancer2::Core::App' ); ::is( $app->name, 'App', 'Correct app name' ); }; } Plack::Test->create( App->to_app )->request( GET '/' ); Makefile.PL100644001751001751 1165113171470520 14710 0ustar00jasonjason000000000000Dancer2-0.205002# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.009. use strict; use warnings; use ExtUtils::MakeMaker 7.1101; use File::ShareDir::Install; $File::ShareDir::Install::INCLUDE_DOTFILES = 1; $File::ShareDir::Install::INCLUDE_DOTDIRS = 1; install_share dist => "share"; my %WriteMakefileArgs = ( "ABSTRACT" => "Lightweight yet powerful web application framework", "AUTHOR" => "Dancer Core Developers", "BUILD_REQUIRES" => { "ExtUtils::MakeMaker" => "7.1101" }, "CONFIGURE_REQUIRES" => { "ExtUtils::MakeMaker" => "7.1101", "File::ShareDir::Install" => "0.06" }, "DISTNAME" => "Dancer2", "EXE_FILES" => [ "script/dancer2" ], "LICENSE" => "perl", "NAME" => "Dancer2", "PREREQ_PM" => { "App::Cmd::Setup" => 0, "Attribute::Handlers" => 0, "Carp" => 0, "Config::Any" => 0, "Digest::SHA" => 0, "Encode" => 0, "Exporter" => "5.57", "Exporter::Tiny" => 0, "File::Basename" => 0, "File::Copy" => 0, "File::Find" => 0, "File::Path" => 0, "File::ShareDir" => 0, "File::Spec" => 0, "File::Temp" => 0, "HTTP::Body" => 0, "HTTP::Date" => 0, "HTTP::Headers::Fast" => 0, "HTTP::Tiny" => 0, "HTTP::XSCookies" => "0.000007", "Hash::Merge::Simple" => 0, "Hash::MultiValue" => 0, "Import::Into" => 0, "JSON::MaybeXS" => 0, "List::Util" => "1.29", "MIME::Base64" => "3.13", "Module::Runtime" => 0, "Moo" => "2.000000", "Moo::Role" => 0, "POSIX" => 0, "Plack" => "1.0035", "Plack::Middleware::FixMissingBodyInRedirect" => 0, "Plack::Middleware::RemoveRedundantBody" => 0, "Ref::Util" => 0, "Return::MultiLevel" => 0, "Role::Tiny" => "2.000000", "Safe::Isa" => 0, "Sub::Quote" => 0, "Template" => 0, "Template::Tiny" => 0, "Test::Builder" => 0, "Test::More" => "0.92", "Type::Tiny" => "1.000006", "URI::Escape" => 0, "YAML" => "0.86", "parent" => 0 }, "TEST_REQUIRES" => { "Capture::Tiny" => "0.12", "ExtUtils::MakeMaker" => "7.1101", "File::Spec" => 0, "HTTP::Body" => 0, "HTTP::Cookies" => 0, "HTTP::Headers" => 0, "IO::Handle" => 0, "IPC::Open3" => 0, "Template" => 0, "Test::Builder" => 0, "Test::EOL" => 0, "Test::Fatal" => 0, "Test::More" => "0.92" }, "VERSION" => "0.205002", "test" => { "TESTS" => "t/*.t t/classes/Dancer2-Core-Factory/*.t t/classes/Dancer2-Core-Hook/*.t t/classes/Dancer2-Core-Request/*.t t/classes/Dancer2-Core-Response-Delayed/*.t t/classes/Dancer2-Core-Response/*.t t/classes/Dancer2-Core-Role-Engine/*.t t/classes/Dancer2-Core-Role-Handler/*.t t/classes/Dancer2-Core-Role-HasLocation/*.t t/classes/Dancer2-Core-Role-Serializer/*.t t/classes/Dancer2-Core-Role-StandardResponses/*.t t/classes/Dancer2-Core-Route/*.t t/classes/Dancer2-Core-Runner/*.t t/classes/Dancer2-Core/*.t t/classes/Dancer2/*.t t/dsl/*.t t/issues/*.t t/issues/gh-1013/*.t t/issues/gh-1046/*.t t/issues/gh-1216/*.t t/issues/gh-1226/*.t t/issues/gh-1230/*.t t/issues/gh-639/fails/*.t t/issues/gh-639/succeeds/*.t t/issues/gh-650/*.t t/issues/gh-975/*.t t/issues/memleak/*.t t/plugin2/*.t t/roles/*.t t/route-pod-coverage/*.t t/scope_problems/*.t t/template_tiny/*.t" } ); my %FallbackPrereqs = ( "App::Cmd::Setup" => 0, "Attribute::Handlers" => 0, "Capture::Tiny" => "0.12", "Carp" => 0, "Config::Any" => 0, "Digest::SHA" => 0, "Encode" => 0, "Exporter" => "5.57", "Exporter::Tiny" => 0, "ExtUtils::MakeMaker" => "7.1101", "File::Basename" => 0, "File::Copy" => 0, "File::Find" => 0, "File::Path" => 0, "File::ShareDir" => 0, "File::Spec" => 0, "File::Temp" => 0, "HTTP::Body" => 0, "HTTP::Cookies" => 0, "HTTP::Date" => 0, "HTTP::Headers" => 0, "HTTP::Headers::Fast" => 0, "HTTP::Tiny" => 0, "HTTP::XSCookies" => "0.000007", "Hash::Merge::Simple" => 0, "Hash::MultiValue" => 0, "IO::Handle" => 0, "IPC::Open3" => 0, "Import::Into" => 0, "JSON::MaybeXS" => 0, "List::Util" => "1.29", "MIME::Base64" => "3.13", "Module::Runtime" => 0, "Moo" => "2.000000", "Moo::Role" => 0, "POSIX" => 0, "Plack" => "1.0035", "Plack::Middleware::FixMissingBodyInRedirect" => 0, "Plack::Middleware::RemoveRedundantBody" => 0, "Ref::Util" => 0, "Return::MultiLevel" => 0, "Role::Tiny" => "2.000000", "Safe::Isa" => 0, "Sub::Quote" => 0, "Template" => 0, "Template::Tiny" => 0, "Test::Builder" => 0, "Test::EOL" => 0, "Test::Fatal" => 0, "Test::More" => "0.92", "Type::Tiny" => "1.000006", "URI::Escape" => 0, "YAML" => "0.86", "parent" => 0 ); unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) { delete $WriteMakefileArgs{TEST_REQUIRES}; delete $WriteMakefileArgs{BUILD_REQUIRES}; $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs; } delete $WriteMakefileArgs{CONFIGURE_REQUIRES} unless eval { ExtUtils::MakeMaker->VERSION(6.52) }; WriteMakefile(%WriteMakefileArgs); { package MY; use File::ShareDir::Install qw(postamble); } response.t100644001751001751 267213171470520 15207 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Dancer2; use Dancer2::Core::Response; my $r = Dancer2::Core::Response->new( content => "hello" ); is $r->status, 200; is $r->content, 'hello'; note "content_type"; $r = Dancer2::Core::Response->new( headers => [ 'Content-Type' => 'text/html' ], content => 'foo', ); is_deeply $r->to_psgi, [ 200, [ Server => "Perl Dancer2 " . Dancer2->VERSION, 'Content-Length' => 3, 'Content-Type' => 'text/html', ], ['foo'] ]; isa_ok $r->headers, 'HTTP::Headers'; is $r->content_type, 'text/html'; $r->content_type('text/plain'); is $r->content_type, 'text/plain'; ok( !$r->is_forwarded ); $r->forward('http://perldancer.org'); ok( $r->is_forwarded ); is $r->header('X-Foo'), undef; $r->header( 'X-Foo' => 42 ); is $r->header('X-Foo'), 42; $r->header( 'X-Foo' => 432 ); is $r->header('X-Foo'), 432; $r->push_header( 'X-Foo' => 777 ); is $r->header('X-Foo'), '432, 777'; $r->header( 'X-Bar' => 234 ); is $r->header('X-Bar'), '234'; is scalar( @{ $r->headers_to_array } ), 12; # stringify HTTP status $r = Dancer2::Core::Response->new( content => "foo", status => "Not Found" ); is $r->status, 404; $r = Dancer2::Core::Response->new( content => "foo", status => "not_modified" ); is $r->status, 304; # test setting content as "0" $r = Dancer2::Core::Response->new( content => "foo" ); $r->content("0"); is $r->content, "0"; done_testing; psgi_app.t100644001751001751 436313171470520 15152 0ustar00jasonjason000000000000Dancer2-0.205002/t#!perl use strict; use warnings; use Test::More tests => 25; use Plack::Test; use HTTP::Request::Common; { package App1; use Dancer2; get '/1' => sub {1}; } { package App2; use Dancer2; get '/2' => sub {2}; } { package App3; use Dancer2; get '/3' => sub {3}; } sub is_available { my ( $cb, @apps ) = @_; foreach my $app (@apps) { is( $cb->( GET "/$app" )->content, $app, "App$app available" ); } } sub isnt_available { my ( $cb, @apps ) = @_; foreach my $app (@apps) { is( $cb->( GET "/$app" )->code, 404, "App$app is not available", ); } } note 'All Apps'; { my $app = Dancer2->psgi_app; isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; is_available( $cb, 1, 2, 3 ); }; } note 'Specific Apps by parameters'; { my @apps = @{ Dancer2->runner->apps }[ 0, 2 ]; is( scalar @apps, 2, 'Took two apps from the Runner' ); my $app = Dancer2->psgi_app(\@apps); isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; is_available( $cb, 1, 3 ); isnt_available( $cb, 2 ); }; } note 'Specific Apps via App objects'; { my $app = App2->psgi_app; isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; is_available( $cb, 2 ); isnt_available( $cb, 1, 3 ); }; }; note 'Specific apps by App names'; { my $app = Dancer2->psgi_app( [ 'App1', 'App3' ] ); isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; isnt_available( $cb, 2 ); is_available( $cb, 1, 3 ); }; } note 'Specific apps by App names with regular expression, v1'; { my $app = Dancer2->psgi_app( [ qr/^App1$/, qr/^App3$/ ] ); isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; isnt_available( $cb, 2 ); is_available( $cb, 1, 3 ); }; } note 'Specific apps by App names with regular expression, v2'; { my $app = Dancer2->psgi_app( [ qr/^App(2|3)$/ ] ); isa_ok( $app, 'CODE', 'Got PSGI app' ); test_psgi $app, sub { my $cb = shift; isnt_available( $cb, 1 ); is_available( $cb, 2, 3 ); }; } redirect.t100644001751001751 1450613171470520 15171 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; subtest 'basic redirects' => sub { { package App1; use Dancer2; get '/' => sub {'home'}; get '/bounce' => sub { redirect '/' }; get '/redirect' => sub { response_header 'X-Foo' => 'foo'; redirect '/'; }; get '/redirect_querystring' => sub { redirect '/login?failed=1' }; } my $app = App1->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { my $res = $cb->( GET '/' ); is( $res->code, 200, '[GET /] Correct code' ); is( $res->content, 'home', '[GET /] Correct content' ); is( $res->headers->content_type, 'text/html', '[GET /] Correct content-type', ); is( $cb->( GET '/bounce' )->code, 302, '[GET /bounce] Correct code', ); } { my $res = $cb->( GET '/redirect' ); is( $res->code, 302, '[GET /redirect] Correct code' ); is( $res->headers->header('Location'), 'http://localhost/', 'Correct Location header', ); is( $res->headers->header('X-Foo'), 'foo', 'Correct X-Foo header', ); } { my $res = $cb->( GET '/redirect_querystring' ); is( $res->code, 302, '[GET /redirect_querystring] Correct code' ); is( $res->headers->header('Location'), 'http://localhost/login?failed=1', 'Correct Location header', ); } }; }; # redirect absolute subtest 'absolute and relative redirects' => sub { { package App2; use Dancer2; get '/absolute_with_host' => sub { redirect "http://foo.com/somewhere"; }; get '/absolute' => sub { redirect "/absolute"; }; get '/relative' => sub { redirect "somewhere/else"; }; } my $app = App2->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { my $res = $cb->( GET '/absolute_with_host' ); is( $res->headers->header('Location'), 'http://foo.com/somewhere', 'Correct Location header', ); } { my $res = $cb->( GET '/absolute' ); is( $res->headers->header('Location'), 'http://localhost/absolute', 'Correct Location header', ); } { my $res = $cb->( GET '/relative' ); is( $res->headers->header('Location'), 'http://localhost/somewhere/else', 'Correct Location header', ); } }; }; subtest 'redirect behind a proxy' => sub { { package App3; use Dancer2; prefix '/test2'; set behind_proxy => 1; get '/bounce' => sub { redirect '/test2' }; } my $app = App3->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { is( $cb->( GET '/test2/bounce', 'X-FORWARDED-HOST' => 'nice.host.name', )->headers->header('Location'), 'http://nice.host.name/test2', 'behind a proxy, host() is read from X_FORWARDED_HOST', ); } { is( $cb->( GET '/test2/bounce', 'X-FORWARDED-HOST' => 'nice.host.name', 'FORWARDED-PROTO' => 'https', )->headers->header('Location'), 'https://nice.host.name/test2', '... and the scheme is read from HTTP_FORWARDED_PROTO', ); } { is( $cb->( GET '/test2/bounce', 'X-FORWARDED-HOST' => 'nice.host.name', 'X-FORWARDED-PROTOCOL' => 'ftp', # stupid, but why not? )->headers->header('Location'), 'ftp://nice.host.name/test2', '... or from X_FORWARDED_PROTOCOL', ); } { is( $cb->( GET '/test2/bounce', 'X-FORWARDED-HOST' => 'nice.host.name', 'X-FORWARDED-PROTO' => 'https', )->headers->header('Location'), 'https://nice.host.name/test2', '... or from X_FORWARDED_PROTO', ); } }; }; subtest 'redirect behind multiple proxies' => sub { { package App4; use Dancer2; prefix '/test2'; set behind_proxy => 1; get '/bounce' => sub { redirect '/test2' }; } my $app = App4->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { is( $cb->( GET '/test2/bounce', 'X-FORWARDED-HOST' => "proxy1.example, proxy2.example", )->headers->header('Location'), 'http://proxy1.example/test2', "behind multiple proxies, host() is read from X_FORWARDED_HOST", ); } { is( $cb->( GET '/test2/bounce', 'X-FORWARDED-HOST' => "proxy1.example, proxy2.example", 'FORWARDED-PROTO' => 'https', )->headers->header('Location'), 'https://proxy1.example/test2', '... and the scheme is read from HTTP_FORWARDED_PROTO', ); } { is( $cb->( GET '/test2/bounce', 'X-FORWARDED-HOST' => "proxy1.example, proxy2.example", 'X-FORWARDED-PROTOCOL' => 'ftp', # stupid, but why not? )->headers->header('Location'), 'ftp://proxy1.example/test2', '... or from X_FORWARDED_PROTOCOL', ); } }; }; done_testing; template.t100644001751001751 1012313171470520 15172 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Dancer2::Core::Hook; use Plack::Test; use HTTP::Request::Common; use File::Spec; use File::Basename 'dirname'; eval { require Template; Template->import(); 1 } or plan skip_all => 'Template::Toolkit probably missing.'; use_ok('Dancer2::Template::TemplateToolkit'); my $views = File::Spec->rel2abs( File::Spec->catfile( dirname(__FILE__), 'views' ) ); my $tt = Dancer2::Template::TemplateToolkit->new( views => $views, layout => 'main.tt', ); isa_ok $tt, 'Dancer2::Template::TemplateToolkit'; ok $tt->does('Dancer2::Core::Role::Template'); $tt->add_hook( Dancer2::Core::Hook->new( name => 'engine.template.before_render', code => sub { my $tokens = shift; $tokens->{before_template_render} = 1; }, ) ); $tt->add_hook( Dancer2::Core::Hook->new( name => 'engine.template.before_layout_render', code => sub { my $tokens = shift; my $content = shift; $tokens->{before_layout_render} = 1; $$content .= "\ncontent added in before_layout_render"; }, ) ); $tt->add_hook( Dancer2::Core::Hook->new( name => 'engine.template.after_layout_render', code => sub { my $content = shift; $$content .= "\ncontent added in after_layout_render"; }, ) ); $tt->add_hook( Dancer2::Core::Hook->new( name => 'engine.template.after_render', code => sub { my $content = shift; $$content .= 'content added in after_template_render'; }, ) ); { package Bar; use Dancer2; # set template engine for first app Dancer2->runner->apps->[0]->set_template_engine($tt); get '/' => sub { template index => { var => 42 } }; # Call template as a global keyword my $global= template( index => { var => 21 } ); get '/global' => sub { $global }; } subtest 'template hooks' => sub { my $space = " "; my $result = "layout top var = 42 before_layout_render = 1 --- [index] var = 42 before_layout_render =$space before_template_render = 1 content added in after_template_render content added in before_layout_render --- layout bottom content added in after_layout_render"; my $test = Plack::Test->create( Bar->to_app ); my $res = $test->request( GET '/' ); is $res->content, $result, '[GET /] Correct content with template hooks'; $result =~ s/42/21/g; $res = $test->request( GET '/global' ); is $res->content, $result, '[GET /global] Correct content with template hooks'; }; { package Foo; use Dancer2; set views => '/this/is/our/path'; get '/default_views' => sub { set 'views' }; get '/set_views_via_settings' => sub { set views => '/other/path' }; get '/get_views_via_settings' => sub { set 'views' }; } subtest "modify views - absolute paths" => sub { my $test = Plack::Test->create( Foo->to_app ); is( $test->request( GET '/default_views' )->content, '/this/is/our/path', '[GET /default_views] Correct content', ); # trigger a test via a route $test->request( GET '/set_views_via_settings' ); is( $test->request( GET '/get_views_via_settings' )->content, '/other/path', '[GET /get_views_via_settings] Correct content', ); }; { package Baz; use Dancer2; set template => 'template_toolkit'; get '/set_views/**' => sub { my ($view) = splat; set views => join('/', @$view ); }; get '/:file' => sub { template param('file'); }; } subtest "modify views propagates to TT2 via dynamic INCLUDE_PATH" => sub { my $test = Plack::Test->create( Baz->to_app ); my $res = $test->request( GET '/index' ); is $res->code, 200, 'got template from views'; # Change views - this is an existing test corpus.. $test->request( GET '/set_views/t/corpus/pretty' ); # Get another template that is known to exist in the test corpus $res = $test->request( GET '/relative.tt' ); is $res->code, 200, 'got template from other view'; }; done_testing; config.yml100644001751001751 10313171470520 15117 0ustar00jasonjason000000000000Dancer2-0.205002/tlog: "info" logger: "Note" plugins: "FooPlugin": plugin: 42 lib000755001751001751 013171470520 13563 5ustar00jasonjason000000000000Dancer2-0.205002/tFoo.pm100644001751001751 12613171470520 14763 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage t::lib::Foo; use Dancer2; get '/in_foo' => sub { session('test'); }; 1; poc.pm100644001751001751 43213171470520 15021 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage poc; use Dancer2; our $VERSION = '0.1'; use Dancer2::Plugin::Foo; set plugins => { Foo => { one => 1, two => 2, size => 4, }, }; get '/' => sub { return 'hello there'; }; get '/truncate' => sub { truncate_txt "hello there" }; true; path.t100644001751001751 634713171470520 15072 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More tests => 5; use Plack::Test; use Plack::Request; use Plack::Builder; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { my $dancer_req = request; my $env = $dancer_req->env; my $plack_req = Plack::Request->new($env); ::like( $env->{'PATH_INFO'}, qr{^/?$}, 'PATH_INFO empty or /', ); ::is( $dancer_req->path_info, $env->{'PATH_INFO'}, 'D2 path_info matches $env', ); ::is( $dancer_req->path_info, $plack_req->path_info, 'D2 path_info matches Plack path_info', ); ::is( $dancer_req->path, '/', 'D2 path is /' ); ::is( $plack_req->path, '/', 'Plack path is /' ); return $dancer_req->script_name; }; get '/endpoint' => sub { my $dancer_req = request; my $env = $dancer_req->env; my $plack_req = Plack::Request->new($env); ::is( $env->{'PATH_INFO'}, '/endpoint', 'PATH_INFO /endpoint', ); ::is( $dancer_req->path_info, $env->{'PATH_INFO'}, 'D2 path_info matches $env', ); ::is( $dancer_req->path_info, $plack_req->path_info, 'D2 path_info matches Plack path_info', ); ::is( $dancer_req->path, '/endpoint', 'D2 path is /' ); ::is( $plack_req->path, '/endpoint', 'Plack path is /' ); return $dancer_req->script_name; }; } subtest '/' => sub { my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '', 'script_name is empty' ); }; subtest '/endpoint' => sub { my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET '/endpoint' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '', 'script_name is empty' ); }; subtest '/mounted/' => sub { my $app = builder { mount '/' => sub { [200,[],['OK']] }; mount '/mounted' => App->to_app; }; my $test = Plack::Test->create($app); my $res = $test->request( GET '/mounted/' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '/mounted', 'script_name is /mounted' ); }; subtest '/mounted/endpoint' => sub { my $app = builder { mount '/' => sub { [200,[],['OK']] }; mount '/mounted' => App->to_app; }; my $test = Plack::Test->create($app); my $res = $test->request( GET '/mounted/endpoint' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '/mounted', 'script_name is /mounted' ); }; # Tests behaviour when SCRIPT_NAME is also the beginning of PATH_INFO # See the discussion in #1288. subtest '/endpoint/endpoint' => sub { my $app = builder { mount '/' => sub { [200,[],['OK']] }; mount '/endpoint' => App->to_app; }; my $test = Plack::Test->create($app); my $res = $test->request( GET '/endpoint/endpoint' ); ok( $res->is_success, 'Result successful' ); is( $res->content, '/endpoint', 'script_name is /endpoint' ); }; json.t100644001751001751 67213171470520 15062 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { my $app = app; my %test = (foo => 'bar'); ::is( encode_json(\%test), '{"foo":"bar"}', 'encode_json works' ); ::is_deeply( decode_json(encode_json(\%test)), \%test, 'decode_json works' ); }; } Plack::Test->create( App->to_app )->request( GET '/' ); pass.t100644001751001751 174313171470520 15077 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; subtest 'pass within routes' => sub { { package App; use Dancer2; get '/' => sub { 'hello' }; get '/**' => sub { response_header 'X-Pass' => 'pass'; pass; redirect '/'; # won't get executed as pass returns immediately. }; get '/pass' => sub { return "the baton"; }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { my $res = $cb->( GET '/pass' ); is( $res->code, 200, '[/pass] Correct status' ); is( $res->content, 'the baton', '[/pass] Correct content' ); is( $res->headers->header('X-Pass'), 'pass', '[/pass] Correct X-Pass header', ); } }; }; done_testing; halt.t100644001751001751 360013171470520 15053 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; subtest 'halt within routes' => sub { { package App; use Dancer2; get '/' => sub { 'hello' }; get '/halt' => sub { response_header 'X-Foo' => 'foo'; halt; }; get '/shortcircuit' => sub { app->response->content('halted'); halt; redirect '/'; # won't get executed as halt returns immediately. }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { my $res = $cb->( GET '/shortcircuit' ); is( $res->code, 200, '[/shortcircuit] Correct status' ); is( $res->content, 'halted', '[/shortcircuit] Correct content' ); } { my $res = $cb->( GET '/halt' ); is( $res->server, "Perl Dancer2 " . Dancer2->VERSION, '[/halt] Correct Server header', ); is( $res->headers->header('X-Foo'), 'foo', '[/halt] Correct X-Foo header', ); } }; }; subtest 'halt in before hook' => sub { { package App; use Dancer2; hook before => sub { response->content('I was halted'); halt if request->path eq '/shortcircuit'; }; } my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/shortcircuit' ); is( $res->code, 200, '[/shortcircuit] Correct code with before hook' ); is( $res->content, 'I was halted', '[/shortcircuit] Correct content with before hook', ); }; }; done_testing; auto_page.t100644001751001751 462513171470520 15315 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; { package AutoPageTest; use Dancer2; set auto_page => 1; set views => 't/views'; set layout => 'main'; set charset => 'UTF-8'; } my @engines = ('tiny'); eval {require Template; Template->import(); push @engines, 'template_toolkit';}; for my $tt_engine ( @engines ) { # Change template engine and run tests AutoPageTest::set( template => $tt_engine ); subtest "autopage with template $tt_engine" => \&run_tests; } sub run_tests { my $test = Plack::Test->create( AutoPageTest->to_app ); { my $r = $test->request( GET '/auto_page' ); is( $r->code, 200, 'Autopage found the page' ); # ö is U+00F6 or c3 b6 when encoded as bytes like( $r->content, qr/---\nHey! This is Auto Page w\x{c3}\x{b6}rking/, '...with proper content', ); is( $r->headers->content_type, 'text/html', 'auto page has correct content type header', ); is( $r->headers->content_type_charset, 'UTF-8', 'auto page has correct charset in content type header', ); is( $r->headers->content_length, 98, # auto_page.tt+layouts/main.tt processed. ö has two bytes in UTF-8 'auto page has correct content length header', ); } { my $r = $test->request( GET '/folder/page' ); is( $r->code, 200, 'Autopage found the page under a folder' ); like( $r->content, qr/---\nPage under folder/, '...with proper content', ); } { my $r = $test->request( GET '/non_existent_page' ); is( $r->code, 404, 'Autopage doesn\'t try to render nonexistent pages' ); } { my $r = $test->request( GET '/layouts/main'); is( $r->code, 404, 'Layouts are not served' ); } { my $r = $test->request( GET '/file.txt' ); is( $r->code, 200, 'found file on public with autopage' ); is( $r->content, "this is a public file\n", '[GET /file.txt] Correct content', ); like( $r->headers->content_type, qr{text/plain}, 'public served file has correct content type header', ); } } done_testing; app_alone.t100644001751001751 63413171470520 15263 0ustar00jasonjason000000000000Dancer2-0.205002/t#!perl use strict; use warnings; use Test::More tests => 3; use Plack::Test; use HTTP::Request::Common; { package MyApp; use Dancer2; get '/' => sub {'OK'}; } my $app = MyApp->to_app; isa_ok( $app, 'CODE' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/' )->code, 200, '[GET /] Correct status' ); is( $cb->( GET '/' )->content, 'OK', '[GET /] Correct content' ); }; App2.pm100644001751001751 25713171470520 15047 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage t::lib::App2; use strict; use warnings; use Dancer2; use lib '.'; use t::lib::DancerPlugin; install_hooks; get '/app2' => sub { session 'before_plugin'; }; 1; poc2.pm100644001751001751 65413171470520 15111 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage poc2; use strict; use warnings; use Dancer2; set logger => 'Capture'; BEGIN { set plugins => { Polite => { smiley => '8-D', }, }; } use PoC::Plugin::Polite ':app'; hook 'smileys' => sub { send_error "Not in sudoers file. This incident will be reported"; }; get '/' => sub { add_smileys( 'make me a sandwich.' ); }; get '/sudo' => sub { hooked_smileys( 'make me a sandwich.' ); }; 1; App1.pm100644001751001751 25713171470520 15046 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage t::lib::App1; use strict; use warnings; use Dancer2; use lib '.'; use t::lib::DancerPlugin; install_hooks; get '/app1' => sub { session 'before_plugin'; }; 1; splat.t100644001751001751 120513171470520 15245 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More tests => 4; use Plack::Test; use HTTP::Request::Common; my @splat; { package App; use Dancer2; get '/*/*/*' => sub { my $params = params(); ::is_deeply( $params, { splat => [ qw ], foo => 42 }, 'Correct params', ); @splat = splat; }; } my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET '/foo/bar/baz?foo=42' ); is_deeply( [@splat], [qw(foo bar baz)], 'splat behaves as expected' ); is( $res->code, 200, 'got a 200' ); is_deeply( $res->content, 3, 'got expected response' ); xt000755001751001751 013171470520 13205 5ustar00jasonjason000000000000Dancer2-0.205002perltidy.rc100644001751001751 224213171470520 15527 0ustar00jasonjason000000000000Dancer2-0.205002/xt-l=79 # Max line width is 79 cols -i=4 # Indent level is 4 cols -ci=4 # Continuation indent is 4 cols -se # Errors to STDERR -vt=2 # Maximal vertical tightness -cti=0 # No extra indentation for closing brackets -pt=1 # Medium parenthesis tightness -bt=1 # Medium brace tightness -sbt=1 # Medium square bracket tightness -bbt=1 # Medium block brace tightness -nsfs # No space before semicolons -nolq # Don't outdent long quoted strings --break-at-old-comma-breakpoints -wbb="% + - * / x != == >= <= =~ < > | & **= += *= &= <<= &&= -= /= |= >>= ||= .= %= ^= x=" # Break before all operators # extras/overrides/deviations from PBP --maximum-line-length=79 # be less generous --warning-output # Show warnings --maximum-consecutive-blank-lines=2 # default is 1 --nohanging-side-comments # troublesome for commented out code -isbc # block comments may only be indented if they have some space characters before the # -ci=2 # Continuation indent is 2 cols # we use version control, so just rewrite the file # -b # -- should not be active for dzil plugin ## for the up-tight folk :) #-pt=2 # High parenthesis tightness #-bt=2 # High brace tightness #-sbt=2 # High square bracket tightness lib000755001751001751 013171470520 13320 5ustar00jasonjason000000000000Dancer2-0.205002Dancer2.pm100644001751001751 2005313171470520 15314 0ustar00jasonjason000000000000Dancer2-0.205002/libpackage Dancer2; $Dancer2::VERSION = '0.205002'; # ABSTRACT: Lightweight yet powerful web application framework use strict; use warnings; use List::Util 'first'; use Module::Runtime 'use_module'; use Import::Into; use Dancer2::Core; use Dancer2::Core::App; use Dancer2::Core::Runner; use Dancer2::FileUtils; our $AUTHORITY = 'SUKRIA'; sub VERSION { shift->SUPER::VERSION(@_) || '0.000000_000' } our $runner; sub runner {$runner} sub psgi_app { shift->runner->psgi_app(@_) } sub import { my ($class, @args) = @_; my ($caller, $script) = caller; my @final_args; my $clean_import; foreach my $arg (@args) { # ignore, no longer necessary # in the future these will warn as deprecated grep +($arg eq $_), qw<:script :syntax :tests> and next; if ($arg eq ':nopragmas') { $clean_import++; next; } if (substr($arg, 0, 1) eq '!') { push @final_args, $arg, 1; } else { push @final_args, $arg; } } $clean_import or $_->import::into($caller) for qw; scalar @final_args % 2 and die q{parameters must be key/value pairs or '!keyword'}; my %final_args = @final_args; my $appname = delete $final_args{appname}; $appname ||= $caller; # never instantiated the runner, should do it now if (not defined $runner) { $runner = Dancer2::Core::Runner->new(); } # Search through registered apps, creating a new app object # if we do not find one with the same name. my $app; ($app) = first { $_->name eq $appname } @{$runner->apps}; if (!$app) { # populating with the server's postponed hooks in advance $app = Dancer2::Core::App->new( name => $appname, caller => $script, environment => $runner->environment, postponed_hooks => $runner->postponed_hooks->{$appname} || {}, ); # register the app within the runner instance $runner->register_application($app); } _set_import_method_to_caller($caller); # use config dsl class, must extend Dancer2::Core::DSL my $config_dsl = $app->setting('dsl_class') || 'Dancer2::Core::DSL'; $final_args{dsl} ||= $config_dsl; # load the DSL, defaulting to Dancer2::Core::DSL my $dsl = use_module($final_args{dsl})->new(app => $app); $dsl->export_symbols_to($caller, \%final_args); } sub _set_import_method_to_caller { my ($caller) = @_; my $import = sub { my ($self, %options) = @_; my $with = $options{with}; for my $key (keys %$with) { $self->dancer_app->setting($key => $with->{$key}); } }; { ## no critic no strict 'refs'; no warnings 'redefine'; *{"${caller}::import"} = $import; } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2 - Lightweight yet powerful web application framework =head1 VERSION version 0.205002 =head1 DESCRIPTION Dancer2 is the new generation of L, the lightweight web-framework for Perl. Dancer2 is a complete rewrite based on L. Dancer2 can optionally use XS modules for speed, but at its core remains fatpackable (packable by L) so you could easily deploy Dancer2 applications on hosts that do not support custom CPAN modules. Dancer2 is easy and fun: use Dancer2; get '/' => sub { "Hello World" }; dance; This is the main module for the Dancer2 distribution. It contains logic for creating a new Dancer2 application. You are welcome to join our mailing list. For subscription information, mail address and archives see L. We are also on IRC: #dancer on irc.perl.org. =head2 Documentation Index Documentation on Dancer2 is split into several manpages. Below is a complete outline on where to go for help. =over 4 =item * Dancer2 Tutorial If you are new to the Dancer approach, you should start by reading our L. =item * Dancer2 Manual L is the reference for Dancer2. Here you will find information on the concepts of Dancer2 application development and a comprehensive reference to the Dancer2 domain specific language. =item * Dancer2 Keywords The keywords for Dancer2 can be found under L. =item * Dancer2 Deployment For configuration examples of different deployment solutions involving Dancer2 and Plack, refer to L. =item * Dancer2 Cookbook Specific examples of code for real-life problems and some 'tricks' for applications in Dancer can be found in L =item * Dancer2 Config For configuration file details refer to L. It is a complete list of all configuration options. =item * Dancer2 Plugins Refer to L for a partial list of available Dancer2 plugins. Note that although we try to keep this list up to date we expect plugin authors to tell us about new modules. For information on how to author a plugin, see L. =item * Dancer2 Migration guide L provides the most up-to-date instruction on how to convert a Dancer (1) based application to Dancer2. =back =head1 FUNCTIONS =head2 my $runner=runner(); Returns the current runner. It is of type L. =head1 AUTHORS =head2 CORE DEVELOPERS Alberto Simões Alexis Sukrieh Damien Krotkine David Precious Franck Cuny Jason A. Crome Mickey Nasriachi Peter Mottram (SysPete) Russell Jenkins Sawyer X Stefan Hornburg (Racke) Steven Humphrey Yanick Champoux =head2 CORE DEVELOPERS EMERITUS David Golden =head2 CONTRIBUTORS A. Sinan Unur Abdullah Diab Ahmad M. Zawawi Alex Beamish Alexander Karelas Alexandr Ciornii Andrew Beverley Andrew Grangaard Andrew Inishev Andrew Solomon Andy Jack Ashvini V B10m Bas Bloemsaat baynes Ben Hutton biafra Blabos de Blebe Breno G. de Oliveira cdmalon Celogeek Cesare Gargano Charlie Gonzalez chenchen000 Chi Trinh Christian Walde Colin Kuskie cym0n Dale Gallagher Daniel Muey Daniel Perrett Dave Jacoby David (sbts) David Steinbrunner David Zurborg Davs Dennis Lichtenthäler Dinis Rebolo dtcyganov Erik Smit Fayland Lam Gabor Szabo geistteufel Gideon D'souza Glenn Fowler Graham Knop Gregor Herrmann Grzegorz Rożniecki Hobbestigrou Hunter McMillen Ivan Bessarabov Ivan Kruglov JaHIY Jakob Voss James Aitken James Raspass James McCoy Jason Lewis Javier Rojas Jean Stebens Jens Rehsack Joel Berger Jonathan Cast Jonathan Scott Duff Julien Fiegehenn Julio Fraire Kaitlyn Parkhurst (SYMKAT) kbeyazli Keith Broughton lbeesley Lennart Hengstmengel Ludovic Tolhurst-Cleaver Mario Zieschang Mark A. Stratman Marketa Wachtlova Masaaki Saito Mateu X Hunter Matt Phillips Matt S Trout Maurice Menno Blom Michael Kröll Michał Wojciechowski Mohammad S Anwar mokko Nick Patch Nick Tonkin Nikita K Nuno Carvalho Olaf Alders Olivier Mengué Omar M. Othman pants Patrick Zimmermann Pau Amma Paul Cochrane Paul Williams Pedro Bruno Pedro Melo Philippe Bricout Ricardo Signes Rick Yakubowski Ruben Amortegui sakshee3 Sam Kington Samit Badle Shlomi Fish simbabque Slava Goltser Snigdha Tatsuhiko Miyagawa Tina Müller Tom Hukins Upasana Shukla Vernon Lyon Victor Adam Vince Willems Vincent Bachelier Yves Orton =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2017 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut named_apps.t100644001751001751 136313171470520 15454 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; { package Foo; use Dancer2; hook before => sub { vars->{foo} = 'foo' }; post '/foo' => sub { return vars->{foo} . 'foo' . vars->{baz}; }; } { package Bar; use Dancer2 appname => 'Foo'; # Add routes and hooks to Foo. hook before => sub { vars->{baz} = 'baz' }; post '/bar' => sub { return vars->{foo} . 'bar' . vars->{baz}; } } my $app = Dancer2->psgi_app; test_psgi $app, sub { my $cb = shift; for my $path ( qw/foo bar/ ) { my $res = $cb->( POST "/$path" ); is $res->content, "foo${path}baz", "Got app content path $path"; } }; done_testing; multi_apps.t100644001751001751 177713171470520 15533 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Builder; use Plack::Test; use HTTP::Request::Common; { package MyTestWiki; use Dancer2; get '/' => sub { __PACKAGE__ }; get '/wiki' => sub {'WIKI'}; package MyTestForum; use Dancer2; get '/' => sub { __PACKAGE__ }; get '/forum' => sub {'FORUM'}; } { my $app = builder { mount '/wiki' => MyTestWiki->to_app; mount '/forum' => MyTestForum->to_app; }; isa_ok( $app, 'CODE', 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/wiki' )->content, 'MyTestWiki', "Got wiki root" ); is( $cb->( GET '/forum' )->content, 'MyTestForum', "Got forum root" ); }; } { my $app = Dancer2->psgi_app; isa_ok( $app, 'CODE', 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/wiki' )->content, 'WIKI', 'Got /wiki path' ); is( $cb->( GET '/forum' )->content, 'FORUM', 'Got /forum path' ); }; } done_testing; log_levels.t100644001751001751 342213171470520 15476 0ustar00jasonjason000000000000Dancer2-0.205002/t#!perl use strict; use warnings; use Test::More tests => 6; use Capture::Tiny 0.12 'capture_stderr'; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set logger => 'console'; set log => 'debug'; get '/debug' => sub { debug "debug msg\n"; warning "warning msg\n"; error "error msg\n"; set log => 'warning'; return 'debug'; }; get '/warning' => sub { debug "debug msg\n"; warning "warning msg\n"; error "error msg\n"; return 'warning'; }; } my $app = App->to_app; test_psgi $app, sub { my $cb = shift; my $res; { my $stderr = capture_stderr { $res = $cb->( GET '/debug' ) }; is( $res->code, 200, 'Successful response' ); is( $res->content, 'debug', 'Correct content' ); like( $stderr, qr/ ^ # a debug line \[App:\d+\] \s debug [^\n]+ \n # a warning line \[App:\d+\] \s warning [^\n]+ \n # followed by an error line \[App:\d+\] \s error [^\n]+ \n $ /x, 'Log levels work', ); } { my $stderr = capture_stderr { $res = $cb->( GET '/warning' ) }; is( $res->code, 200, 'Successful response' ); is( $res->content, 'warning', 'Correct content' ); like( $stderr, qr/ ^ # a warning line \[App:\d+\] \s warning [^\n]+ \n # followed by an error line \[App:\d+\] \s error [^\n]+ \n $ /x, 'Log levels work', ); } }; custom_dsl.t100644001751001751 105013171470520 15512 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use FindBin qw($Bin); use lib "$Bin/lib"; use Dancer2 dsl => 'MyDancerDSL'; envoie '/' => sub { request->method; }; prend '/' => sub { proto { ::ok('in proto') }; # no sub! request->method; }; my $test = Plack::Test->create( __PACKAGE__->to_app ); is( $test->request( GET '/' )->content, 'GET', '[GET /] Correct content' ); is( $test->request( POST '/' )->content, 'POST', '[POST /] Correct content' ); done_testing(); serializer.t100644001751001751 164313171470520 15517 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More tests => 5; use Dancer2::Serializer::Dumper; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package MyApp; use Dancer2; set serializer => 'JSON'; get '/json' => sub { +{ bar => 'baz' } }; } my $app = MyApp->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; { # Response with implicit call to the serializer my $res = $cb->( GET '/json' ); is( $res->code, 200, '[/json] Correct status' ); is( $res->content, '{"bar":"baz"}', '[/json] Correct content' ); is( $res->headers->content_type, 'application/json', '[/json] Correct content-type headers', ); } }; my $serializer = Dancer2::Serializer::Dumper->new(); is( $serializer->content_type, 'text/x-data-dumper', 'content-type is set correctly', ); file_utils.t100644001751001751 462213171470520 15505 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use utf8; use Test::More tests => 25; use Test::Fatal; use File::Spec; BEGIN { @File::Spec::ISA = ("File::Spec::Unix") } use File::Temp 0.22; use Dancer2::FileUtils qw/read_file_content path_or_empty path/; sub write_file { my ( $file, $content ) = @_; open my $fh, '>', $file or die "cannot write file $file : $!"; binmode $fh, ':encoding(utf-8)'; print $fh $content; close $fh; } sub hexe { my $s = shift; $s =~ s/([\x00-\x1F])/sprintf('%#x',ord($1))/eg; return $s; } like( exception { Dancer2::FileUtils::open_file( '<', '/slfkjsdlkfjsdlf' ) }, qr{/slfkjsdlkfjsdlf' using mode '<': \w+}, 'Failure opening nonexistent file', ); my $content = Dancer2::FileUtils::read_file_content(); is $content, undef; my $paths = [ [ undef => 'undef' ], [ '/foo/./bar/' => '/foo/bar/' ], [ '/foo/../bar' => '/bar' ], [ '/foo/bar/..' => '/foo/' ], [ '/a/b/c/d/A/B/C' => '/a/b/c/d/A/B/C' ], [ '/a/b/c/d/../A/B/C' => '/a/b/c/A/B/C' ], [ '/a/b/c/d/../../A/B/C' => '/a/b/A/B/C' ], [ '/a/b/c/d/../../../A/B/C' => '/a/A/B/C' ], [ '/a/b/c/d/../../../../A/B/C' => '/A/B/C' ], ]; for my $case ( @$paths ) { is Dancer2::FileUtils::normalize_path( $case->[0] ), $case->[1]; } my $p = Dancer2::FileUtils::dirname('/somewhere'); is $p, '/'; my $tmp = File::Temp->new(); my $two = "²❷"; write_file( $tmp, "one$/$two" ); $content = read_file_content($tmp); is hexe($content), hexe("one$/$two"); my @content = read_file_content($tmp); is hexe( $content[0] ), hexe("one$/"); is $content[1], "$two"; # returns UNDEF on non-existant path my $path = 'bla/blah'; if ( !-e $path ) { is( path_or_empty($path), '', 'path_or_empty on non-existent path', ); } my $tmpdir = File::Temp->newdir; is( path_or_empty($tmpdir), $tmpdir, 'path_or_empty on an existing path' ); #slightly tricky paths on different platforms is( path( '/', 'b', '/c' ), '/b//c', 'path /,b,/c -> /b//c' ); is( path( '/', '/b', ), '/b', 'path /, /b -> /b' ); note "escape_filename"; { my $names = [ [ undef => 'undef' ], [ 'abcdef' => 'abcdef' ], [ 'ab++ef' => 'ab+2b+2bef' ], [ 'a/../b.txt' => 'a+2f+2e+2e+2fb+2etxt' ], [ "test\0\0" => 'test+00+00' ], [ 'test☠☠☠' => 'test+2620+2620+2620' ], ]; for my $case ( @$names ) { is Dancer2::FileUtils::escape_filename( $case->[0] ), $case->[1]; } } dispatcher.t100644001751001751 1505413171470520 15515 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Carp 'croak'; use Ref::Util qw; use Dancer2; use Dancer2::Core::App; use Dancer2::Core::Route; use Dancer2::Core::Dispatcher; use Dancer2::Core::Hook; use Dancer2::Core::Response; set logger => 'Null'; # init our test fixture my $buffer = {}; my $app = Dancer2::Core::App->new( name => 'main' ); $app->setting( logger => engine('logger') ); $app->setting( show_errors => 1 ); # a simple / route my $simple_route = $app->add_route( method => 'get', regexp => '/', code => sub {"home"}, ); # an error route my $error_route = $app->add_route( method => 'get', regexp => '/error', code => sub { Fail->fail }, ); # A chain of two route for /user/$foo my $user_name_route = $app->add_route( method => 'get', regexp => '/user/:name', code => sub { my $app = shift; $buffer->{user} = $app->request->params->{'name'}; $app->response->has_passed(1); }, ); my $user_splat_route = $app->add_route( method => 'get', regexp => '/user/*?', code => sub { my $app = shift; "Hello " . $app->request->params->{'name'}; }, ); # a route with a 204 response my $removed_content_route = $app->add_route( method => 'get', regexp => '/twoohfour', code => sub { my $app = shift; $app->response->status(204); "This content should be removed"; }, ); my $route_from_request; # simulates a redirect with halt $app->add_hook( Dancer2::Core::Hook->new( name => 'before', code => sub { my $app = shift; $route_from_request = $app->request->route; if ( $app->request->path_info eq '/haltme' ) { $app->response->header( Location => 'http://perldancer.org' ); $app->response->status(302); $app->response->is_halted(1); } }, ) ); my $was_in_second_filter = 0; $app->add_hook( Dancer2::Core::Hook->new( name => 'before', code => sub { my $app = shift; if ( $app->request->path_info eq '/haltme' ) { $was_in_second_filter = 1; # should not happen because first filter halted the flow } }, ) ); my $halt_route = $app->add_route( method => 'get', regexp => '/haltme', code => sub {"should not get there"}, ); # the tests my @tests = ( { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/', }, expected => [ 200, [ 'Content-Length' => 4, 'Content-Type' => 'text/html; charset=UTF-8', 'Server' => "Perl Dancer2 " . Dancer2->VERSION, ], ["home"], $simple_route, ] }, { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/user/Johnny', }, expected => [ 200, [ 'Content-Length' => 12, 'Content-Type' => 'text/html; charset=UTF-8', 'Server' => "Perl Dancer2 " . Dancer2->VERSION, ], ["Hello Johnny"], $user_splat_route, # the second, after the first pass()es ] }, { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/twoohfour', }, expected => [ 204, [ 'Content-Type' => 'text/html; charset=UTF-8', 'Server' => "Perl Dancer2 " . Dancer2->VERSION, ], [], $removed_content_route, ] }, { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/haltme', }, expected => [ 302, [ 'Location' => 'http://perldancer.org', 'Content-Length' => '305', 'Content-Type' => 'text/html; charset=utf-8', 'Server' => "Perl Dancer2 " . Dancer2->VERSION, ], qr/This item has moved/, $halt_route, ] }, # NOT SUPPORTED YET # { env => { # REQUEST_METHOD => 'GET', # PATH_INFO => '/admin', # }, # expected => [200, [], ["home"]] # }, ); $app->compile_hooks; plan tests => 20; my $dispatcher = Dancer2::Core::Dispatcher->new( apps => [$app] ); my $counter = 0; foreach my $test (@tests) { my $env = $test->{env}; my $expected = $test->{expected}; my $path = $env->{'PATH_INFO'}; $route_from_request = undef; diag sprintf "Dispatch test %d, for %s %s", $counter, $test->{env}{REQUEST_METHOD}, $test->{env}{PATH_INFO}; my $resp = $dispatcher->dispatch($env); is( $resp->[0], $expected->[0], "[$path] Return code ok" ); my %got_headers = @{ $resp->[1] }; my %exp_headers = @{ $expected->[1] }; is_deeply( \%got_headers, \%exp_headers, "[$path] Correct headers" ); if ( is_regexpref( $expected->[2] ) ) { like $resp->[2][0] => $expected->[2], "[$path] Contents ok. (test $counter)"; } else { is_deeply $resp->[2] => $expected->[2], "[$path] Contents ok. (test $counter)"; } is( $route_from_request, # squirreled away by before hook, $expected->[3], "Expected route is stored in request (test $counter)", ); $counter++; } foreach my $test ( { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/error', 'psgi.uri_scheme' => 'http', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', }, expected => [ 500, [ 'Content-Length', "Content-Type", 'text/html' ], qr!Internal Server Error.*Can't locate object method "fail" via package "Fail" \(perhaps you forgot to load "Fail"\?\) at t[\\/]dispatcher\.t line \d+\.!s ] } ) { my $env = $test->{env}; my $expected = $test->{expected}; my $psgi_response = $dispatcher->dispatch($env); my $resp = Dancer2::Core::Response->new( status => $psgi_response->[0], headers => $psgi_response->[1], content => $psgi_response->[2][0], ); is $resp->status => $expected->[0], "Return code ok."; ok( $resp->header('Content-Length') >= 140, "Length ok." ); like $resp->content, $expected->[2], "contents ok"; } is $was_in_second_filter, 0, "didn't enter the second filter, because of halt"; roles000755001751001751 013171470520 14141 5ustar00jasonjason000000000000Dancer2-0.205002/thook.t100644001751001751 263113171470520 15430 0ustar00jasonjason000000000000Dancer2-0.205002/t/rolesuse strict; use warnings; use Test::More tests => 8; use Test::Fatal; use Dancer2::Core::Hook; my $h = Dancer2::Core::Hook->new( name => 'before_template', code => sub {'BT'} ); is $h->name, 'before_template_render'; is $h->code->(), 'BT'; { package Foo; use Moo; with 'Dancer2::Core::Role::Hookable'; sub hook_aliases { +{} } sub supported_hooks {'foobar'} } my $f = Foo->new; like( exception { $f->execute_hook() }, qr{execute_hook needs a hook name}, 'execute_hook needs a hook name', ); my $count = 0; my $some_hook = Dancer2::Core::Hook->new( name => 'foobar', code => sub { $count++; } ); ok( !exception { $f->add_hook($some_hook) }, 'Supported hook can be installed', ); like( exception { $f->add_hook( Dancer2::Core::Hook->new( name => 'unknown_hook', code => sub { $count++; } ) ); }, qr{Unsupported hook 'unknown_hook'}, 'Unsupported hook cannot be installed', ); $f->execute_hook('foobar'); is $count, 1; like( exception { $f->replace_hook( 'doesnotexist', [] ) }, qr{Hook 'doesnotexist' must be installed first}, 'Nonexistent hook fails', ); my $new_hooks = [ sub { $count-- }, sub { $count-- }, sub { $count-- } ]; $f->replace_hook( 'foobar', $new_hooks ); $f->execute_hook('foobar'); is $count, -2, 'replaced hooks were installed and executed'; extend.t100644001751001751 202313171470520 15410 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsl# define a sample DSL extension that will be used in the rest of these test # This extends Dancer2::Core::DSL but provides an extra keyword # # Each test below creates a new package so it can load Dancer2 BEGIN { package Dancer2::Test::ExtendedDSL; use Moo; extends 'Dancer2::Core::DSL'; sub BUILD { my ( $self ) = @_; $self->register(foo => 1); } sub foo { return $_[1]; } } package main; use Test::More tests => 5; package test1; use Test::More; use Dancer2 dsl => 'Dancer2::Test::ExtendedDSL'; ok(defined &foo, 'use line dsl can foo'); is(foo('bar'), 'bar', 'use line Foo returns bar'); package test2; use Test::More; ok(!defined &foo, 'intermediate package has no polluted namespace'); package test3; use Test::More; use FindBin; use File::Spec; BEGIN { $ENV{DANCER_CONFDIR} = File::Spec->catdir($FindBin::Bin, 'extend_config'); } use Dancer2; ok(defined &foo, 'config specified DSL can foo'); is(foo('baz'), 'baz', 'config specified Foo returns baz'); done_testing; to_app.t100644001751001751 103613171470520 15406 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Plack::Test; use HTTP::Request::Common; use Test::More tests => 2; { package App1; use Dancer2; get '/' => sub {'App1'}; my $app = to_app; ::test_psgi $app, sub { my $cb = shift; ::is( $cb->( ::GET '/' )->content, 'App1', 'Got first App' ); }; } { package App2; use Dancer2; get '/' => sub {'App2'}; my $app = to_app; ::test_psgi $app, sub { my $cb = shift; ::is( $cb->( ::GET '/' )->content, 'App2', 'Got second App' ); }; } script000755001751001751 013171470520 14056 5ustar00jasonjason000000000000Dancer2-0.205002dancer2100755001751001751 1223313171470520 15503 0ustar00jasonjason000000000000Dancer2-0.205002/script#!/usr/bin/perl # PODNAME: dancer2 # ABSTRACT: Dancer2 command line interface use strict; use warnings; use Dancer2::CLI; # backward compatibility if (@ARGV && ($ARGV[0] =~ m/^-(a|p|x|s)/ || $ARGV[0] =~ m/^--(application|path|no-check|skel)/)) { # GetOptions and Getopt::Long::Descriptive differently treats # cases like '-a=Test'. GetOptions returs 'Test' as value of 'a', # while Getopt::Long::Descriptive returns '=Test' as value foreach (@ARGV) { s/^\-(a|p)=/-$1/; } unshift @ARGV, 'gen'; } exit Dancer2::CLI->run; __END__ =pod =encoding UTF-8 =head1 NAME dancer2 - Dancer2 command line interface =head1 VERSION version 0.205002 =head1 SYNOPSIS dancer2 [options...] =head1 DESCRIPTION Dancer2 is the new generation lightweight web-framework for Perl. This tool provides nice, easily-extendable CLI interface for it. =head2 Documentation Index Documentation on Dancer2 is split into several manpages. Below is a complete outline on where to go for help. =over 4 =item * Dancer2 Tutorial If you are new to the Dancer approach, you should start by reading our L. =item * Dancer2 Manual L is the reference for Dancer2. Here you will find information on the concepts of Dancer2 application development and a comprehensive reference to the Dancer2 domain specific language. =item * Dancer2 Keywords The keywords for Dancer2 can be found under L. =item * Dancer2 Deployment For configuration examples of different deployment solutions involving Dancer2 and Plack, refer to L. =item * Dancer2 Cookbook Specific examples of code for real-life problems and some 'tricks' for applications in Dancer can be found in L =item * Dancer2 Config For configuration file details refer to L. It is a complete list of all configuration options. =item * Dancer2 Plugins Refer to L for a partial list of available Dancer2 plugins. Note that although we try to keep this list up to date we expect plugin authors to tell us about new modules. =item * Dancer2 Migration guide L provides the most up-to-date instruction on how to convert a Dancer (1) based application to Dancer2. =back =head1 NAME dancer2 - Dancer2 command line interface =head1 COMMANDS =over =item gen : create new Dancer2 application =item commands : list the application's commands =item help : display a command's help screen =item version : display version =back To get detailed description of each individual command run: dancer2 help The lastest list of available commands can be dispayed by: dancer2 commands =head1 COMMAND 'gen' Helper script for providing a bootstrapping method to quickly and easily create the framework for a new Dancer2 application. =head3 OPTIONS -a --application the name of your application -p --path the path where to create your application (current directory if not specified) -o --overwrite overwrite existing files -x --no-check don't check for the latest version of Dancer2 (checking version implies internet connection) -s --skel skeleton directory =head3 EXAMPLE Here is an application created with dancer2: $ dancer2 gen -a MyWeb::App + MyWeb-App + MyWeb-App/bin + MyWeb-App/bin/app.psgi + MyWeb-App/config.yml + MyWeb-App/environments + MyWeb-App/environments/development.yml + MyWeb-App/environments/production.yml + MyWeb-App/views + MyWeb-App/views/index.tt + MyWeb-App/views/layouts + MyWeb-App/views/layouts/main.tt + MyWeb-App/MANIFEST.SKIP + MyWeb-App/lib + MyWeb-App/lib/MyWeb + MyWeb-App/lib/MyWeb/App.pm + MyWeb-App/public + MyWeb-App/public/css + MyWeb-App/public/css/style.css + MyWeb-App/public/css/error.css + MyWeb-App/public/images + MyWeb-App/public/500.html + MyWeb-App/public/404.html + MyWeb-App/public/dispatch.fcgi + MyWeb-App/public/dispatch.cgi + MyWeb-App/public/javascripts + MyWeb-App/public/javascripts/jquery.js + MyWeb-App/t + MyWeb-App/t/002_index_route.t + MyWeb-App/t/001_base.t + MyWeb-App/Makefile.PL The application is ready to serve: $ cd MyWeb-App $ plackup bin/app.psgi >> Listening on 127.0.0.1:3000 == Entering the development dance floor ... =head1 AUTHOR This script has been written by Ivan Kruglov base on original dancer2 script which has been written by Sebastien Deseille and Alexis Sukrieh . =head1 SOURCE CODE See L for more information. =head1 LICENSE This module is free software and is published under the same terms as Perl itself. =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2017 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut 00-compile.t100644001751001751 1061113171470520 15226 0ustar00jasonjason000000000000Dancer2-0.205002/tuse 5.006; use strict; use warnings; # this test was generated with Dist::Zilla::Plugin::Test::Compile 2.056 use Test::More; plan tests => 58 + ($ENV{AUTHOR_TESTING} ? 1 : 0); my @module_files = ( 'Dancer2.pm', 'Dancer2/CLI.pm', 'Dancer2/CLI/Command/gen.pm', 'Dancer2/CLI/Command/version.pm', 'Dancer2/Core.pm', 'Dancer2/Core/App.pm', 'Dancer2/Core/Cookie.pm', 'Dancer2/Core/DSL.pm', 'Dancer2/Core/Dispatcher.pm', 'Dancer2/Core/Error.pm', 'Dancer2/Core/Factory.pm', 'Dancer2/Core/HTTP.pm', 'Dancer2/Core/Hook.pm', 'Dancer2/Core/MIME.pm', 'Dancer2/Core/Request.pm', 'Dancer2/Core/Request/Upload.pm', 'Dancer2/Core/Response.pm', 'Dancer2/Core/Response/Delayed.pm', 'Dancer2/Core/Role/ConfigReader.pm', 'Dancer2/Core/Role/DSL.pm', 'Dancer2/Core/Role/Engine.pm', 'Dancer2/Core/Role/Handler.pm', 'Dancer2/Core/Role/HasLocation.pm', 'Dancer2/Core/Role/Hookable.pm', 'Dancer2/Core/Role/Logger.pm', 'Dancer2/Core/Role/Serializer.pm', 'Dancer2/Core/Role/SessionFactory.pm', 'Dancer2/Core/Role/SessionFactory/File.pm', 'Dancer2/Core/Role/StandardResponses.pm', 'Dancer2/Core/Role/Template.pm', 'Dancer2/Core/Route.pm', 'Dancer2/Core/Runner.pm', 'Dancer2/Core/Session.pm', 'Dancer2/Core/Time.pm', 'Dancer2/Core/Types.pm', 'Dancer2/FileUtils.pm', 'Dancer2/Handler/AutoPage.pm', 'Dancer2/Handler/File.pm', 'Dancer2/Logger/Capture.pm', 'Dancer2/Logger/Capture/Trap.pm', 'Dancer2/Logger/Console.pm', 'Dancer2/Logger/Diag.pm', 'Dancer2/Logger/File.pm', 'Dancer2/Logger/Note.pm', 'Dancer2/Logger/Null.pm', 'Dancer2/Plugin.pm', 'Dancer2/Serializer/Dumper.pm', 'Dancer2/Serializer/JSON.pm', 'Dancer2/Serializer/Mutable.pm', 'Dancer2/Serializer/YAML.pm', 'Dancer2/Session/Simple.pm', 'Dancer2/Session/YAML.pm', 'Dancer2/Template/Implementation/ForkedTiny.pm', 'Dancer2/Template/Simple.pm', 'Dancer2/Template/TemplateToolkit.pm', 'Dancer2/Template/Tiny.pm', 'Dancer2/Test.pm' ); my @scripts = ( 'script/dancer2' ); # no fake home requested my @switches = ( -d 'blib' ? '-Mblib' : '-Ilib', ); use File::Spec; use IPC::Open3; use IO::Handle; open my $stdin, '<', File::Spec->devnull or die "can't open devnull: $!"; my @warnings; for my $lib (@module_files) { # see L my $stderr = IO::Handle->new; diag('Running: ', join(', ', map { my $str = $_; $str =~ s/'/\\'/g; q{'} . $str . q{'} } $^X, @switches, '-e', "require q[$lib]")) if $ENV{PERL_COMPILE_TEST_DEBUG}; my $pid = open3($stdin, '>&STDERR', $stderr, $^X, @switches, '-e', "require q[$lib]"); binmode $stderr, ':crlf' if $^O eq 'MSWin32'; my @_warnings = <$stderr>; waitpid($pid, 0); is($?, 0, "$lib loaded ok"); shift @_warnings if @_warnings and $_warnings[0] =~ /^Using .*\bblib/ and not eval { require blib; blib->VERSION('1.01') }; if (@_warnings) { warn @_warnings; push @warnings, @_warnings; } } foreach my $file (@scripts) { SKIP: { open my $fh, '<', $file or warn("Unable to open $file: $!"), next; my $line = <$fh>; close $fh and skip("$file isn't perl", 1) unless $line =~ /^#!\s*(?:\S*perl\S*)((?:\s+-\w*)*)(?:\s*#.*)?$/; @switches = (@switches, split(' ', $1)) if $1; my $stderr = IO::Handle->new; diag('Running: ', join(', ', map { my $str = $_; $str =~ s/'/\\'/g; q{'} . $str . q{'} } $^X, @switches, '-c', $file)) if $ENV{PERL_COMPILE_TEST_DEBUG}; my $pid = open3($stdin, '>&STDERR', $stderr, $^X, @switches, '-c', $file); binmode $stderr, ':crlf' if $^O eq 'MSWin32'; my @_warnings = <$stderr>; waitpid($pid, 0); is($?, 0, "$file compiled ok"); shift @_warnings if @_warnings and $_warnings[0] =~ /^Using .*\bblib/ and not eval { require blib; blib->VERSION('1.01') }; # in older perls, -c output is simply the file portion of the path being tested if (@_warnings = grep { !/\bsyntax OK$/ } grep { chomp; $_ ne (File::Spec->splitpath($file))[2] } @_warnings) { warn @_warnings; push @warnings, @_warnings; } } } is(scalar(@warnings), 0, 'no warnings found') or diag 'got warnings: ', ( Test::More->can('explain') ? Test::More::explain(\@warnings) : join("\n", '', @warnings) ) if $ENV{AUTHOR_TESTING}; whitespace.t100644001751001751 31213171470520 15642 0ustar00jasonjason000000000000Dancer2-0.205002/xtuse Test::Whitespaces { dirs => [qw( lib script t tools xt )], ignore => [ qr{t/sessions/}, qr{t/template_tiny/samples}, ], }; deserialize.t100644001751001751 1403613171470520 15666 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More tests => 15; use Plack::Test; use HTTP::Request::Common; use Dancer2::Logger::Capture; my $logger = Dancer2::Logger::Capture->new; isa_ok( $logger, 'Dancer2::Logger::Capture' ); { package App; use Dancer2; # default, we're actually overriding this later set serializer => 'JSON'; # for now set logger => 'Console'; put '/from_params' => sub { my %p = params(); return [ map +( $_ => $p{$_} ), sort keys %p ]; }; put '/from_data' => sub { my $p = request->data; return [ map +( $_ => $p->{$_} ), sort keys %{$p} ]; }; # This route is used for both toure and body params. post '/from/:town' => sub { my $p = params; return [ map +( $_ => $p->{$_} ), sort keys %{$p} ]; }; any [qw/del patch/] => '/from/:town' => sub { my $p = params('body'); return [ map +( $_ => $p->{$_} ), sort keys %{$p} ]; }; } my $test = Plack::Test->create( App->to_app ); subtest 'PUT request with parameters' => sub { for my $type ( qw ) { my $res = $test->request( PUT "/from_$type", 'Content-Type' => 'application/json', Content => '{ "foo": 1, "bar": 2 }' ); is( $res->content, '["bar",2,"foo",1]', "Parameters deserialized from $type", ); } }; my $app = App->to_app; use utf8; use JSON::MaybeXS; use Encode; use Module::Runtime 'use_module'; note "Verify Serializers decode into characters"; { my $utf8 = '∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i)'; test_psgi $app, sub { my $cb = shift; for my $type ( qw/Dumper JSON YAML/ ) { my $class = "Dancer2::Serializer::$type"; use_module($class); my $serializer = $class->new(); my $body = $serializer->serialize({utf8 => $utf8}); # change the app serializer # we're overiding a RO attribute only for this test! Dancer2->runner->apps->[0]->set_serializer_engine( $serializer ); my $r = $cb->( PUT '/from_params', 'Content-Type' => $serializer->content_type, Content => $body, ); my $content = Encode::decode( 'UTF-8', $r->content ); # Dumper is a jerk and represents it in Perl \x{...} notation if ( $type eq 'Dumper' ) { { no strict; $content = eval $content; } # now $content is an actual ref again is_deeply( $content, [ 'utf8', $utf8 ], "utf-8 string returns the same using the $type serializer", ) } else { like( $content, qr{\Q$utf8\E}, "utf-8 string returns the same using the $type serializer", ); } } }; } # default back to JSON for the rest # we're overiding a RO attribute only for this test! Dancer2->runner->apps->[0]->set_serializer_engine( Dancer2::Serializer::JSON->new ); note "Decoding of mixed route and deserialized body params"; { # Check integers from request body remain integers # but route params get decoded. test_psgi $app, sub { my $cb = shift; my @req_params = ( "/from/D\x{c3}\x{bc}sseldorf", # /from/d%C3%BCsseldorf 'Content-Type' => 'application/json', Content => JSON::MaybeXS::encode_json({ population => 592393 }), ); my $r = $cb->( POST @req_params ); # Watch out for hash order randomization.. is_deeply( $r->content, '["population",592393,"town","'."D\x{c3}\x{bc}sseldorf".'"]', "Integer from JSON body remains integer and route params decoded", ); }; } # Check body is deserialized on PATCH and DELETE. # The RFC states the behaviour for DELETE is undefined; We take the lenient # and deserialize it. # http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-24#section-4.3.5 note "Deserialze any body content that is allowed or undefined"; { test_psgi $app, sub { my $cb = shift; for my $method ( qw/DELETE PATCH/ ) { my $request = HTTP::Request->new( $method, "/from/D\x{c3}\x{bc}sseldorf", # /from/d%C3%BCsseldorf [ 'Content-Type' => 'application/json' ], JSON::MaybeXS::encode_json({ population => 592393 }), ); my $response = $cb->($request); my $content = Encode::decode( 'UTF-8', $response->content ); # Only body params returned is( $content, '["population",592393]', "JSON body deserialized for " . uc($method) . " requests", ); } } } note 'Check serialization errors'; { Dancer2->runner->apps->[0]->set_serializer_engine( Dancer2::Serializer::JSON->new( log_cb => sub { $logger->log(@_) } ) ); test_psgi $app, sub { my $cb = shift; $cb->( PUT '/from_params', 'Content-Type' => 'application/json', Content => '---', ); my $trap = $logger->trapper; isa_ok( $trap, 'Dancer2::Logger::Capture::Trap' ); my $errors = $trap->read; isa_ok( $errors, 'ARRAY' ); is( scalar @{$errors}, 1, 'One error caught' ); my $msg = $errors->[0]; delete $msg->{'formatted'}; isa_ok( $msg, 'HASH' ); is( scalar keys %{$msg}, 2, 'Two items in the error' ); is( $msg->{'level'}, 'core', 'Correct level' ); like( $msg->{'message'}, qr{ ^ \QFailed to deserialize content: \E \Qmalformed number\E }x, 'Correct error message', ); } } http_status.t100644001751001751 275713171470520 15737 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More tests => 5; use Dancer2::Core::HTTP; subtest "HTTP status" => sub { is( Dancer2::Core::HTTP->status( $_->{status} ) => $_->{expected}, 'status: '. ( $_->{status} || 'undef' ) ) for { status => undef, expected => undef }, { status => 200, expected => 200 }, { status => 'Not Found', expected => 404 }, { status => 'bad_request', expected => 400 }, { status => 'i_m_a_teapot', expected => 418 }, { status => 'error', expected => 500 }, { status => 911, expected => 911 }; }; subtest "HTTP status_message" => sub { is( Dancer2::Core::HTTP->status_message( $_->{status} ) => $_->{expected}, 'status: '. ( $_->{status} || 'undef' ) ) for { status => undef, expected => undef }, { status => 200, expected => 'OK' }, { status => 'error', expected => 'Internal Server Error' }, { status => 911, expected => undef }; }; is { Dancer2::Core::HTTP->status_mapping }->{"I'm a teapot"} => 418, 'status_mapping'; is { Dancer2::Core::HTTP->code_mapping }->{418} => "I'm a teapot", 'code_mapping'; subtest 'all_mappings' => sub { my %mappings = Dancer2::Core::HTTP->all_mappings; is $mappings{"I'm a teapot"} => 418; is $mappings{"i_m_a_teapot"} => 418; is $mappings{418} => "I'm a teapot"; }; dancer-test.t100644001751001751 1037113171470520 15575 0ustar00jasonjason000000000000Dancer2-0.205002/t# who test the tester? We do! use strict; use warnings; use File::Spec; use File::Basename qw/dirname/; use Ref::Util qw; BEGIN { # Disable route handlers so we can actually test route_exists # and route_doesnt_exist. Use config that disables default route handlers. $ENV{DANCER_CONFDIR} = File::Spec->catdir(dirname(__FILE__), 'dancer-test'); } use Test::More tests => 50; use Dancer2; use Dancer2::Test; use Dancer2::Core::Request; use File::Temp; use Encode; use URI::Escape; $Dancer2::Test::NO_WARN = 1; my @routes = ( '/foo', [ GET => '/foo' ], Dancer2::Core::Request->new( env => { 'psgi.url_scheme' => 'http', REQUEST_METHOD => 'GET', QUERY_STRING => '', SERVER_NAME => 'localhost', SERVER_PORT => 5000, SERVER_PROTOCOL => 'HTTP/1.1', SCRIPT_NAME => '', PATH_INFO => '/foo', REQUEST_URI => '/foo', } ), ); my $fighter = Dancer2::Core::Response->new( content => 'fighter', status => 404, ); route_doesnt_exist $_ for (@routes, $fighter); get '/foo' => sub {'fighter'}; route_exists $_, "route $_ exists" for @routes; $fighter->status(200); push @routes, $fighter; for (@routes) { my $response = dancer_response $_; isa_ok $response => 'Dancer2::Core::Response'; is $response->content => 'fighter'; } response_content_is $_ => 'fighter', "response_content_is with $_" for @routes; response_content_isnt $_ => 'platypus', "response_content_isnt with $_" for @routes; response_content_like $_ => qr/igh/ for @routes; response_content_unlike $_ => qr/ought/ for @routes; response_status_is $_ => 200 for @routes; response_status_isnt $_ => 203 for @routes; response_headers_include $_ => [ Server => "Perl Dancer2 " . Dancer2->VERSION ] for @routes; ## Check parameters get through ok get '/param' => sub { param('test') }; my $param_response = dancer_response( GET => '/param', { params => { test => 'hello' } } ); is $param_response->content, 'hello', 'PARAMS get echoed by route'; post '/upload' => sub { my $file = upload('test'); return $file->content; }; ## Check we can upload files my $file_response = dancer_response( POST => '/upload', { files => [ { filename => 'test.txt', name => 'test', data => 'testdata' } ] } ); is $file_response->content, 'testdata', 'file uploaded with supplied data'; my $temp = File::Temp->new; print $temp 'testfile'; close($temp); $file_response = dancer_response( POST => '/upload', { files => [ { filename => $temp->filename, name => 'test' } ] } ); is $file_response->content, 'testfile', 'file uploaded with supplied filename'; ## Check multiselect/multi parameters get through ok get '/multi' => sub { my $t = param('test'); return 'bad' if !is_arrayref($t); my $p = join( '', @$t ); return $p; }; $param_response = dancer_response( GET => '/multi', { params => { test => [ 'foo', 'bar' ] } } ); is $param_response->content, 'foobar', 'multi values for same key get echoed back'; my $russian_test = decode( 'UTF-8', uri_unescape("%D0%B8%D1%81%D0%BF%D1%8B%D1%82%D0%B0%D0%BD%D0%B8%D0%B5") ); $param_response = dancer_response( GET => '/multi', { params => { test => [ 'test/', $russian_test ] } } ); is $param_response->content, 'test/' . encode( 'UTF-8', $russian_test ), 'multi utf8 value properly merge'; get '/headers' => sub { join " : ", request->header('X-Sent-By'), request->cookies->{foo}; }; note "extra headers in request"; sub extra_headers { my $sent_by = 'Dancer2::Test'; my $headers_test = dancer_response( GET => '/headers', { headers => [ [ 'X-Sent-By' => $sent_by ], [ 'Cookie' => "foo=bar" ], ], } ); is $headers_test->content, "$sent_by : bar", "extra headers included in request"; } note "Run extra_headers test with XS_HTTP_COOKIES" if $Dancer2::Core::Request::XS_HTTP_COOKIES; extra_headers(); SKIP: { skip "HTTP::XSCookies not installed", 1 if !$Dancer2::Core::Request::XS_HTTP_COOKIES; note "Run extra_headers test without XS_HTTP_COOKIES"; $Dancer2::Core::Request::XS_HTTP_COOKIES = 0; extra_headers(); } prepare_app.t100644001751001751 221013171470520 15633 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Plack::Test; use HTTP::Request::Common; use Test::More 'tests' => 2; use Capture::Tiny qw< capture_stderr capture_merged >; { package App; use Dancer2; sub prepare_app {1} get '/' => sub {'OK'}; } { package Bar; use Dancer2 'appname' => 'App'; sub prepare_app {2} get '/' => sub {'OK'}; } my $msg = qr{ ^ WARNING: \s You \s have \s a \s subroutine \s in \s your \s app \s called \s 'prepare_app' \. \s In \s the \s future \s this \s will \s automatically \s be \s called \s by \s Dancer2 \. }xms; subtest 'App' => sub { my $app; like( capture_stderr { $app = App->to_app; }, $msg, 'Got warning on prepare_app sub', ); my $test = Plack::Test->create($app); my $res; my $out; capture_merged { $res = $test->request( GET '/' )->content }, is( $res, 'OK', 'Correct content', ); is( $out, undef, 'No extra warnings or output' ); }; subtest 'Bar' => sub { my $app; like( capture_stderr { $app = Bar->to_app; }, $msg, 'Got warning on prepare_app sub', ); }; content.t100644001751001751 253613171470520 15604 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More tests => 1; use Plack::Test; use HTTP::Request::Common; my $logger; { package App::ContentFail; ## no critic use Dancer2; set show_errors => 1; set logger => 'Capture'; $logger = app->engine('logger'); get '/' => sub { content 'Foo' }; } subtest 'content keyword can only be used within delayed response' => sub { my $test = Plack::Test->create( App::ContentFail->to_app ); my $res = $test->request( GET '/' ); ok( ! $res->is_success, 'Request failed' ); is( $res->code, 500, 'Correct response code' ); like( $res->content, qr/Cannot use content keyword outside delayed response/, 'Failed to use content keyword outside delayed response', ); isa_ok( $logger, 'Dancer2::Logger::Capture' ); my $trapper = $logger->trapper; isa_ok( $trapper, 'Dancer2::Logger::Capture::Trap' ); my $error = $trapper->read; isa_ok( $error, 'ARRAY' ); is( scalar @{$error}, 1, 'Only one error' ); ok( delete $error->[0]{'formatted'}, 'Got formatted message' ); like( delete $error->[0]{'message'}, qr{^\QRoute exception: Cannot use content keyword outside delayed response\E}, 'Correct error message', ); is_deeply( $error, [ { level => 'error' } ], 'Rest of error okay', ); }; request.t100644001751001751 211313171470520 15611 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; { package App::DSL::Request; use Dancer2; any [ 'get', 'post' ], '/' => sub { request->method; }; get 'headers' => sub { request_header 'X-Foo'; }; } subtest 'Testing an app with request keyword' => sub { my $test = Plack::Test->create( App::DSL::Request->to_app ); { my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful GET request' ); is( $res->content, 'GET', 'GET / correct content' ); } { my $res = $test->request( POST '/' ); ok( $res->is_success, 'Successful POST request' ); is( $res->content, 'POST', 'POST / correct content' ); } }; subtest 'Testing app with request_header heyword' => sub { my $test = Plack::Test->create( App::DSL::Request->to_app ); my $res = $test->request( GET '/headers', 'X-Foo' => 'Bar' ); ok( $res->is_success, 'Successful GET request' ); is( $res->content, 'Bar', 'GET /headers correct content' ); }; done_testing; delayed.t100644001751001751 1006613171470520 15556 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; eval { require AnyEvent; 1; } or plan skip_all => 'AnyEvent required for this test'; plan tests => 5; { package App::Content; ## no critic use Dancer2; get '/' => sub { ::is( $Dancer2::Core::Route::RESPONDER, undef, 'No responder yet' ); delayed { ::isa_ok( $Dancer2::Core::Route::RESPONDER, 'CODE', 'Got a responder in the delayed callback', ); ::is( $Dancer2::Core::Route::WRITER, undef, 'No writer yet' ); content 'OK'; ::ok( $Dancer2::Core::Route::WRITER, 'Got a writer' ); done; }; }; } { package App::Content::MultiWrite; ## no critic use Dancer2; get '/' => sub { delayed { flush; content 'Foo'; content 'Bar'; done; }; }; } { package App::NoContent; ## no critic use Dancer2; get '/' => sub { delayed {content;done;'Not OK'}; }; } { package App::MultipleContent; ## no critic use Dancer2; get '/' => sub { delayed { content 'Bar'; done; }; return 'OK'; }; } my $caught_error; { package App::ErrorHandler; ## no critic use Dancer2; require AnyEvent; set logger => 'Capture'; get '/log' => sub { delayed { flush; content "ping\n"; done; content "failure\n"; }; }; get '/cb' => sub { delayed { flush; content "ping\n"; done; content "failure\n"; } on_error => sub { $caught_error = shift; }; }; } subtest 'Testing an app with content keyword' => sub { my $test = Plack::Test->create( App::Content->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, 'OK', 'Correct content' ); }; subtest 'Testing an app with multiple content keyword calls' => sub { my $test = Plack::Test->create( App::Content::MultiWrite->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, 'FooBar', 'Correct content' ); }; subtest 'Testing an app without content keyword' => sub { my $test = Plack::Test->create( App::NoContent->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, '', 'Correct content' ); }; subtest 'Delayed response ignored for non-delayed content' => sub { my $test = Plack::Test->create( App::MultipleContent->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, 'OK', 'Correct content' ); }; subtest 'Delayed response error handling' => sub { my $test = Plack::Test->create( App::ErrorHandler->to_app ); TODO: { local $TODO = 'Does not work in development server'; my $res = $test->request( GET '/log' ); ok( $res->is_success, 'Successful request' ); is( $res->content, "ping\n", 'Correct content' ); my $logger = App::ErrorHandler::app->logger_engine; my $logs = $logger->trapper->read; isa_ok( $logs, 'ARRAY', 'Got logs' ); is( scalar @{$logs}, 1, 'Got a message' ); my $msg = shift @{$logs}; ok( $msg, 'Got message' ); isa_ok( $msg, 'HASH', 'Got message' ); is( $msg->{'level'}, 'core', 'Correct error message level', ); like( $msg->{'message'}, qr/^Error in delayed response:/, 'Got error', ); } TODO: { local $TODO = 'Does not work in development server'; my $res = $test->request( GET '/cb' ); ok( $res->is_success, 'Successful request' ); is( $res->content, "ping\n", 'Correct content' ); like( $caught_error, qr/^Error in delayed response:/, 'Got error' ); } }; send_as.t100644001751001751 704413171470520 15545 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; { package DummyObj; use Moo; has foo => (is => 'ro', default => 'bar'); sub TO_JSON { {foo => shift->foo}; } 1; } { package Test::App::SendAs; use Dancer2; set engines => { serializer => { JSON => { allow_blessed => 1, convert_blessed => 1, } } }; set logger => 'Capture'; set serializer => 'YAML'; set template => 'TemplateToolkit'; get '/html' => sub { send_as html => '' }; get '/json/**' => sub { send_as JSON => splat; }; get '/json-object' => sub { send_as JSON => { data => DummyObj->new() }; }; get '/json-utf8/**' => sub { send_as JSON => splat, { content_type => 'application/json', charset => 'utf-8' }; }; get '/yaml/**' => sub { my @params = splat; \@params; }; get '/sendas/:type?' => sub { send_as route_parameters->{'type'} => 'test string'; }; } my $test = Plack::Test->create( Test::App::SendAs->to_app ); subtest "default serializer" => sub { my $res = $test->request( GET '/yaml/is/useful' ); is $res->code, '200'; is $res->content_type, 'text/x-yaml'; my $expected = <<'YAML'; --- - - is - useful YAML is $res->content, $expected; }; subtest "send_as json" => sub { my $res = $test->request( GET '/json/is/wonderful' ); is $res->code, '200'; is $res->content_type, 'application/json'; is $res->content, '["is","wonderful"]'; }; subtest "send_as json object" => sub { my $res = $test->request( GET '/json-object' ); is $res->code, '200'; is $res->content_type, 'application/json'; is $res->content, '{"data":{"foo":"bar"}}'; }; subtest "send_as json custom content-type" => sub { my $res = $test->request( GET '/json-utf8/is/wonderful' ); is $res->code, '200'; is $res->content_type, 'application/json'; is $res->content_type_charset, 'UTF-8'; is $res->content, '["is","wonderful"]'; }; subtest "send_as html" => sub { my $res = $test->request( GET '/html' ); is $res->code, '200'; is $res->content_type, 'text/html'; is $res->content_type_charset, 'UTF-8'; is $res->content, ''; }; subtest "send_as error cases" => sub { my $logger = Test::App::SendAs::app->logger_engine; { my $res = $test->request( GET '/sendas/' ); is $res->code, '500', "send_as dies with no defined type"; my $logs = $logger->trapper->read; like $logs->[0]->{message}, qr!Route exception: Can not send_as using an undefined type!, ".. throws route exception"; } { my $res = $test->request( GET '/sendas/jSoN' ); is $res->code, '500', "send_as dies with incorrectly cased serializer name"; my $logs = $logger->trapper->read; like $logs->[0]->{message}, qr!Route exception: Unable to load serializer class for jSoN!, ".. throws route exception"; } { my $res = $test->request( GET '/sendas/SomeSerializerThatDoesNotExist' ); is $res->code, '500', "send_as dies when called with non-existant serializer"; my $logs = $logger->trapper->read; like $logs->[0]->{message}, qr!Route exception: Unable to load serializer class for SomeSerializerThatDoesNotExist!, ".. throws route exception"; } }; done_testing(); perlcritic.rc100644001751001751 464213171470520 16041 0ustar00jasonjason000000000000Dancer2-0.205002/xt# nice output, to easily see the POD of the policy verbose = [%p] %m at %f line %l, near '%r'\n # severity of 3 is a good start (1 is very strict, 5 very tolerant) severity = 3 # we want to use // without //ms [-RegularExpressions::RequireDotMatchAnything] [-RegularExpressions::RequireLineBoundaryMatching] [-RegularExpressions::RequireExtendedFormatting] minimum_regex_length_to_complain_about = 5 [-RegularExpressions::ProhibitComplexRegexes] # we don't want these POD rules [-Documentation::RequirePodSections] # We don't care about POD links [-Documentation::RequirePodLinksIncludeText] # we use $@ and $! [-Variables::ProhibitPunctuationVars] # We want to be able to use Carp::Verbose in our tests scripts, so # we add Carp to the whitelist [Variables::ProhibitPackageVars] packages = Data::Dumper File::Find FindBin Log::Log4perl Carp [-ValuesAndExpressions::ProhibitEmptyQuotes] # I really don't think q{/} is more readable than '/'... [-ValuesAndExpressions::ProhibitNoisyQuotes] # Perl::Critic recommends Readonly, but this IS BAD! # we use Const::Fast instead, but this policy keeps poping up. [-ValuesAndExpressions::ProhibitMagicNumbers] # we want to be able to build DSLs [-Modules::ProhibitAutomaticExportation] # We only want the main module to provide $VERSION [-Modules::RequireVersionVar] # we want to be able to define short getters [-Subroutines::RequireFinalReturn] # we can't do @_ mesures with that one [-Subroutines::RequireArgUnpacking] # name is a common used name for methods # but forbidden by this policy ... [-Subroutines::ProhibitBuiltinHomonyms] # some old libs use many args, we don't want to block that for now [-Subroutines::ProhibitManyArgs] # we allo protected subs [-Subroutines::ProhibitUnusedPrivateSubroutines] # We're not under CVS! :) [-Miscellanea::RequireRcsKeywords] [TestingAndDebugging::ProhibitNoStrict] allow = refs [TestingAndDebugging::ProhibitNoWarnings] allow = redefine prototype [TestingAndDebugging::RequireUseStrict] equivalent_modules = strictures Moo Moo::Role [TestingAndDebugging::RequireUseWarnings] equivalent_modules = strictures Moo Moo::Role # we use postifx controls [-ControlStructures::ProhibitPostfixControls] [-ControlStructures::ProhibitCascadingIfElse] # We want to use croak everywhere instead of die [ErrorHandling::RequireCarping] # allow backtick if capture result [InputOutput::ProhibitBacktickOperators] only_in_void_context = 1 [-Variables::ProhibitAugmentedAssignmentInDeclaration] template_ext.t100644001751001751 115613171470520 16040 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; eval { require Template; Template->import(); 1 } or plan skip_all => 'Template::Toolkit probably missing.'; use Dancer2; set engines => { template => { template_toolkit => { extension => 'foo', }, }, }; set template => 'template_toolkit'; my $tt = engine('template'); isa_ok( $tt, 'Dancer2::Template::TemplateToolkit' ); is( $tt->default_tmpl_ext, 'foo', "Template extension is 'foo' as configured", ); is( $tt->view_pathname('foo'), 'foo.foo' , "view('foo') gives filename with right extension as configured", ); done_testing; http_methods.t100644001751001751 246013171470520 16046 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More tests => 12; use Plack::Test; use HTTP::Request; use Ref::Util qw; use Dancer2; my %method = ( get => 'GET', post => 'POST', del => 'DELETE', patch => 'PATCH', put => 'PUT', options => 'OPTIONS', ); my $app = __PACKAGE__->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; while ( my ( $method, $http ) = each %method ) { eval "$method '/' => sub { '$method' }"; is( $cb->( HTTP::Request->new( $http => '/' ) )->content, $method, "$http /", ); } eval "get '/head' => sub {'HEAD'}"; my $res = $cb->( HTTP::Request->new( HEAD => '/head' ) ); is( $res->content, '', 'HEAD /' ); # HEAD requests have no content is( $res->headers->content_length, 4, 'Content-Length for HEAD' ); # Testing invalid HTTP methods. { my $req = HTTP::Request->new( "ILLEGAL" => '/' ); my $res = $cb->( $req ); ok( !$res->is_success, "Response->is_success is false when using illegal HTTP method" ); is( $res->code, 405, "Illegal method should return 405 code" ); like( $res->content, qr, q ); } }; TestApp.pm100644001751001751 476613171470520 15656 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage t::lib::TestApp; use Dancer2; # this app is intended to cover 100% of the DSL! # set some MIME aliases... mime->add_type( foo => 'text/foo' ); mime->add_alias( f => 'foo' ); set 'default_mime_type' => 'text/bar'; # hello route get '/' => sub { app->name }; # /haltme should bounce to / hook 'before' => sub { if ( request->path_info eq '/haltme' ) { redirect '/'; halt; } }; get '/haltme' => sub {"should not be there"}; hook 'after' => sub { my $response = shift; if ( request->path_info eq '/rewrite_me' ) { $response->content("rewritten!"); } }; get '/rewrite_me' => sub {"body should not be this one"}; # some settings set some_var => 1; setting some_other_var => 1; set multiple_vars => 4, can_be_set => 2; get '/config' => sub { return config->{some_var} . ' ' . config->{some_other_var} . ' and ' . setting('multiple_vars') . setting('can_be_set'); }; if ( $] >= 5.010 ) { # named captures get qr{/(? usr | content | post )/(? delete | find )/(? \d+ )}x => sub { join( ":", sort %{ captures() } ); }; } # chained routes with pass get '/user/**' => sub { my $user = params->{splat}; var user => $user->[0][0]; pass; }; get '/user/*/home' => sub { my $user = var('user'); # should be set by the previous route "hello $user"; }; # post & dirname post '/dirname' => sub { dirname('/etc/passwd'); }; # header get '/header/:name/:value' => sub { response_header param('name') => param('value'); 1; }; # push_header get '/header/:name/:valueA/:valueB' => sub { push_response_header param('name') => param('valueA'); push_response_header param('name') => param('valueB'); 1; }; # header get '/header_twice/:name/:valueA/:valueB' => sub { response_header param('name') => param('valueA'); response_header param('name') => param('valueB'); 1; }; # any any [ 'get', 'post' ], '/any' => sub { "Called with method " . request->method; }; # true and false get '/booleans' => sub { join( ":", true, false ); }; # mimes get '/mime/:name' => sub { mime->for_name( param('name') ); }; # content_type get '/content_type/:type' => sub { content_type param('type'); 1; }; # prefix prefix '/prefix' => sub { get '/bar' => sub {'/prefix/bar'}; prefix '/prefix1' => sub { get '/bar' => sub {'/prefix/prefix1/bar'}; }; prefix '/prefix2'; get '/foo' => sub {'/prefix/prefix2/foo'}; }; 1; TestPod.pm100644001751001751 112713171470520 15644 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage t::lib::TestPod; use Dancer2; =head1 NAME TestPod =head2 ROUTES =over =cut =item get "/in_testpod" testpod =cut get '/in_testpod' => sub { # code; }; =item get "/hello" testpod =cut get '/hello' => sub { # code; }; =item post '/in_testpod/*' post in_testpod =cut post '/in_testpod/*' => sub { return 'post in_testpod'; }; =back =head2 SPECIALS =head3 PUBLIC =over =item get "/me:id" =cut get "/me:id" => sub { # code; }; =back =head3 PRIVAT =over =item post "/me:id" post /me:id =cut post "/me:id" => sub { # code; }; =back =cut 1; SubApp1.pm100644001751001751 25313171470520 15514 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage t::lib::SubApp1; use strict; use warnings; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::DancerPlugin; install_hooks; get '/subapp1' => sub { 1; }; 1; SubApp2.pm100644001751001751 25313171470520 15515 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage t::lib::SubApp2; use strict; use warnings; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::DancerPlugin; install_hooks; get '/subapp2' => sub { 2; }; 1; views000755001751001751 013171470520 14152 5ustar00jasonjason000000000000Dancer2-0.205002/tindex.tt100644001751001751 20113171470520 15743 0ustar00jasonjason000000000000Dancer2-0.205002/t/views[index] var = [% var %] before_layout_render = [% before_layout_render %] before_template_render = [% before_template_render %] t2000755001751001751 013171470520 14122 5ustar00jasonjason000000000000Dancer2-0.205002/t/app.dancer100644001751001751 113171470520 15426 0ustar00jasonjason000000000000Dancer2-0.205002/t/app/t2 plugin_import.t100644001751001751 236313171470520 16236 0ustar00jasonjason000000000000Dancer2-0.205002/t# plugin_import.t use strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { use Dancer2; use lib 't/lib'; use Dancer2::Plugin::PluginWithImport; get '/test' => sub { dancer_plugin_with_import_keyword; }; } my $app = __PACKAGE__->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/test' )->content, 'dancer_plugin_with_import_keyword', 'the plugin exported its keyword', ); }; is_deeply( Dancer2::Plugin::PluginWithImport->stuff, { 'Dancer2::Plugin::PluginWithImport' => 'imported' }, "the original import method of the plugin is still there" ); subtest 'import flags' => sub { eval " package Dancer2::Plugin::Some::Plugin1; use Dancer2::Plugin ':no_dsl'; register 'foo' => sub { request }; "; like $@, qr{Bareword "request" not allowed while "strict subs"}, "with :no_dsl, the Dancer's dsl is not imported."; eval " package Dancer2::Plugin::Some::Plugin2; use Dancer2::Plugin; register 'foo' => sub { request }; "; is $@, '', "without any import flag, the DSL is imported"; }; done_testing; template_name.t100644001751001751 74113171470520 16137 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use File::Spec; use File::Basename 'dirname'; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package Foo; use Dancer2; get '/template_name' => sub { return engine('template')->name; }; } my $app = Foo->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/template_name' )->content, 'Tiny', 'template name' ); }; done_testing; session_hooks.t100644001751001751 1664713171470520 16266 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; my @hooks_to_test = qw( engine.session.before_retrieve engine.session.after_retrieve engine.session.before_create engine.session.after_create engine.session.before_change_id engine.session.after_change_id engine.session.before_destroy engine.session.after_destroy engine.session.before_flush engine.session.after_flush ); # we'll set a flag here when each hook is called. Then our test will then verify this my $test_flags = {}; { package App; use Dancer2; set( show_errors => 1, envoriment => 'production' ); setting( session => 'Simple' ); for my $hook (@hooks_to_test) { hook $hook => sub { $test_flags->{$hook} ||= 0; $test_flags->{$hook}++; } } get '/set_session' => sub { session foo => 'bar'; #setting causes a session flush return "ok"; }; get '/get_session' => sub { ::is session->read('foo'), 'bar', "Got the right session back"; return "ok"; }; get '/change_session_id' => sub { app->change_session_id; return "ok"; }; get '/destroy_session' => sub { app->destroy_session; return "ok"; }; #setup each hook again and test whether they return the correct type #there is unfortunately quite some duplication here. hook 'engine.session.before_create' => sub { my ($response) = @_; ::isa_ok( $response, 'Dancer2::Core::Session' ); }; hook 'engine.session.after_create' => sub { my ($response) = @_; ::isa_ok( $response, 'Dancer2::Core::Session' ); }; hook 'engine.session.after_retrieve' => sub { my ($response) = @_; ::isa_ok( $response, 'Dancer2::Core::Session' ); }; } my $test = Plack::Test->create( App->to_app ); my $jar = HTTP::Cookies->new; my $url = "http://localhost"; is_deeply( $test_flags, {}, 'Make sure flag hash is clear' ); subtest set_session => sub { my $res = $test->request( GET "$url/set_session" ); is $res->content, "ok", "set_session ran ok"; $jar->extract_cookies($res); }; # we verify whether the hooks were called correctly. subtest 'verify hooks for session create and session flush' => sub { is $test_flags->{'engine.session.before_create'}, 1, "session.before_create called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create called"; is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush called"; is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush called"; is $test_flags->{'engine.session.before_change_id'}, undef, "session.before_change_id not called"; is $test_flags->{'engine.session.after_change_id'}, undef, "session.after_change_id not called"; is $test_flags->{'engine.session.before_retrieve'}, undef, "session.before_retrieve not called"; is $test_flags->{'engine.session.after_retrieve'}, undef, "session.after_retrieve not called"; is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called"; is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called"; }; subtest 'verify Handler::File (static content) does not retrieve session' => sub { my $req = GET "$url/file.txt"; $jar->add_cookie_header($req); my $res = $test->request($req); $jar->extract_cookies($res); # These should not change from previous subtest is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called"; is $test_flags->{'engine.session.before_retrieve'}, undef, "session.before_retrieve not called"; is $test_flags->{'engine.session.after_retrieve'}, undef, "session.after_retrieve not called"; }; subtest get_session => sub { my $req = GET "$url/get_session"; $jar->add_cookie_header($req); my $res = $test->request($req); is $res->content, "ok", "get_session ran ok"; $jar->extract_cookies($res); }; subtest 'verify hooks for session retrieve' => sub { is $test_flags->{'engine.session.before_retrieve'}, 1, "session.before_retrieve called"; is $test_flags->{'engine.session.after_retrieve'}, 1, "session.after_retrieve called"; is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called"; is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called"; is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called"; is $test_flags->{'engine.session.before_change_id'}, undef, "session.before_change_id not called"; is $test_flags->{'engine.session.after_change_id'}, undef, "session.after_change_id not called"; is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called"; is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called"; }; subtest change_session_id => sub { my $req = GET "$url/change_session_id"; $jar->add_cookie_header($req); my $res = $test->request($req); is $res->content, "ok", "get_session ran ok"; $jar->clear; $jar->extract_cookies($res); }; subtest 'verify hooks for change session id' => sub { # change_session_id causes a retrieve is $test_flags->{'engine.session.before_retrieve'}, 2, "session.before_retrieve called"; is $test_flags->{'engine.session.after_retrieve'}, 2, "session.after_retrieve called"; is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called"; is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called"; is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called"; is $test_flags->{'engine.session.before_change_id'}, 1, "session.before_change_id called"; is $test_flags->{'engine.session.after_change_id'}, 1, "session.after_change_id called"; is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called"; is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called"; }; subtest destroy_session => sub { my $req = GET "$url/destroy_session"; $jar->add_cookie_header($req); my $res = $test->request($req); is $res->content, "ok", "destroy_session ran ok"; }; subtest 'verify session destroy hooks' => sub { is $test_flags->{'engine.session.before_destroy'}, 1, "session.before_destroy called"; is $test_flags->{'engine.session.after_destroy'}, 1, "session.after_destroy called"; #not sure if before and after retrieve should be called when the session is destroyed. But this happens. is $test_flags->{'engine.session.before_retrieve'}, 3, "session.before_retrieve called"; is $test_flags->{'engine.session.after_retrieve'}, 3, "session.after_retrieve called"; is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called"; is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called"; is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called"; is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called"; }; done_testing; config_reader.t100644001751001751 1126413171470520 16155 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Test::Fatal; use Carp 'croak'; use Dancer2::Core::Runner; use Dancer2::FileUtils qw/dirname path/; use File::Spec; use File::Temp; # undefine ENV vars used as defaults for app environment in these tests local $ENV{DANCER_ENVIRONMENT}; local $ENV{PLACK_ENV}; my $runner = Dancer2::Core::Runner->new(); my $location = File::Spec->rel2abs( path( dirname(__FILE__), 'config' ) ); my $location2 = File::Spec->rel2abs( path( dirname(__FILE__), 'config2' ) ); { package Prod; use Moo; with 'Dancer2::Core::Role::ConfigReader'; sub name {'Prod'} sub _build_environment {'production'} sub _build_location {$location} sub _build_default_config {$runner->config} package Dev; use Moo; with 'Dancer2::Core::Role::ConfigReader'; sub _build_environment {'development'} sub _build_location {$location}; sub _build_default_config {$runner->config} package Failure; use Moo; with 'Dancer2::Core::Role::ConfigReader'; sub _build_environment {'failure'} sub _build_location {$location} sub _build_default_config {$runner->config} package Staging; use Moo; with 'Dancer2::Core::Role::ConfigReader'; sub _build_environment {'staging'} sub _build_location {$location} sub _build_default_config {$runner->config} package Merging; use Moo; with 'Dancer2::Core::Role::ConfigReader'; sub name {'Merging'} sub _build_environment {'merging'} sub _build_location {$location} sub _build_default_config {$runner->config} package LocalConfig; use Moo; with 'Dancer2::Core::Role::ConfigReader'; sub name {'LocalConfig'} sub _build_environment {'lconfig'} sub _build_location {$location2} sub _build_default_config {$runner->config} } my $d = Dev->new(); is_deeply $d->config_files, [ path( $location, 'config.yml' ), ], "config_files() only sees existing files"; my $f = Prod->new; is $f->does('Dancer2::Core::Role::ConfigReader'), 1, "role Dancer2::Core::Role::ConfigReader is consumed"; is_deeply $f->config_files, [ path( $location, 'config.yml' ), path( $location, 'environments', 'production.yml' ), ], "config_files() works"; my $j = Staging->new; is_deeply $j->config_files, [ path( $location, 'config.yml' ), path( $location, 'environments', 'staging.json' ), ], "config_files() does JSON too!"; note "bad YAML file"; my $fail = Failure->new; is $fail->environment, 'failure'; is_deeply $fail->config_files, [ path( $location, 'config.yml' ), path( $location, 'environments', 'failure.yml' ), ], "config_files() works"; like( exception { $fail->config }, qr{Unable to parse the configuration file}, 'Configuration file parsing failure', ); note "config merging"; my $m = Merging->new; # Check the 'application' top-level key; its the only key that # is currently a HoH in the test configurations is_deeply $m->config->{application}, { some_feature => 'bar', another_setting => 'baz', }, "full merging of configuration hashes"; { my $l = LocalConfig->new; is_deeply $l->config_files, [ path( $location2, 'config.yml' ), path( $location2, 'config_local.yml' ), path( $location2, 'environments', 'lconfig.yml' ), path( $location2, 'environments', 'lconfig_local.yml' ), ], "config_files() with local config works"; is_deeply $l->config->{application}, { feature_1 => 'foo', feature_2 => 'alpha', feature_3 => 'replacement', feature_4 => 'blat', feature_5 => 'beta', feature_6 => 'bar', feature_7 => 'baz', feature_8 => 'goober', }, "full merging of local configuration hashes"; } note "config parsing"; is $f->config->{show_errors}, 0; is $f->config->{main}, 1; is $f->config->{charset}, 'utf-8', "normalized UTF-8 to utf-8"; ok( $f->has_setting('charset') ); ok( !$f->has_setting('foobarbaz') ); note "default values"; is $f->setting('apphandler'), 'Standalone'; like( exception { $f->_normalize_config( { charset => 'BOGUS' } ) }, qr{Charset defined in configuration is wrong : couldn't identify 'BOGUS'}, 'Configuration file charset failure', ); { package Foo; use Carp 'croak'; sub foo { croak "foo" } } is $f->setting('traces'), 0; unlike( exception { Foo->foo() }, qr{Foo::foo}, "traces are not enabled", ); $f->setting( traces => 1 ); like( exception { Foo->foo() }, qr{Foo::foo}, "traces are enabled", ); { my $tmpdir = File::Temp::tempdir( CLEANUP => 1, TMPDIR => 1 ); $ENV{DANCER_CONFDIR} = $tmpdir; my $f = Prod->new; is $f->config_location, $tmpdir; } done_testing; plugin_syntax.t100644001751001751 1001413171470520 16262 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use JSON::MaybeXS; use Ref::Util qw; subtest 'global and route keywords' => sub { { package App1; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::FooPlugin; sub location {'/tmp'} get '/' => sub { foo_wrap_request->env->{'PATH_INFO'}; }; get '/app' => sub { app->name }; get '/plugin_setting' => sub { to_json(p_config) }; foo_route; } my $app = App1->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/' )->content, '/', 'route defined by a plugin', ); is( $cb->( GET '/foo' )->content, 'foo', 'DSL keyword wrapped by a plugin', ); is( _normalize($cb->( GET '/plugin_setting' )->content), _normalize(encode_json( { plugin => '42' } )), 'plugin_setting returned the expected config' ); is( $cb->( GET '/app' )->content, 'App1', 'app name is correct', ); }; }; subtest 'plugin old syntax' => sub { { package App2; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::DancerPlugin; around_get; } my $app = App2->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/foo/plugin' )->content, 'foo plugin', 'foo plugin', ); }; }; subtest caller_dsl => sub { my $app = App1->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/sitemap' )->content, '^\/$, ^\/app$, ^\/foo$, ^\/foo\/plugin$, ^\/plugin_setting$, ^\/sitemap$', 'Correct content', ); }; }; subtest 'hooks in plugins' => sub { my $counter = 0; { package App3; use Dancer2; use lib 't/lib'; use Dancer2::Plugin::OnPluginImport; use Dancer2::Plugin::Hookee; use Dancer2::Plugin::EmptyPlugin; hook 'third_hook' => sub { var( hook => 'third hook' ); }; hook 'start_hookee' => sub { 'this is the start hook'; }; get '/hook_with_var' => sub { some_other(); # executes 'third_hook' ::is var('hook') => 'third hook', "Vars preserved from hooks"; }; get '/hooks_plugin' => sub { $counter++; some_keyword(); # executes 'start_hookee' 'hook for plugin'; }; get '/hook_returns_stuff' => sub { some_keyword(); # executes 'start_hookee' }; get '/on_import' => sub { some_import(); # execute 'plugin_import' } } my $app = App3->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $counter, 0, 'the hook has not been executed' ); is( $cb->( GET '/hooks_plugin' )->content, 'hook for plugin', '... route is rendered', ); is( $counter, 1, '... and the hook has been executed exactly once' ); is( $cb->( GET '/hook_returns_stuff' )->content, '', '... hook does not influence rendered content by return value', ); # call the route that has an additional test $cb->( GET '/hook_with_var' ); is ( $cb->( GET '/on_import' )->content, Dancer2->VERSION, 'hooks added by on_plugin_import don\'t stop hooks being added later' ); }; }; sub _normalize { my ($json) = @_; my $data = decode_json($json); foreach (keys %$data) { $data->{$_} = $data->{$_} * 1 if ($data->{$_} =~ m/^\d+$/); } return encode_json($data); } done_testing; memory_cycles.t100644001751001751 111213171470520 16207 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Test::Fatal; use Plack::Test; eval { require Test::Memory::Cycle; 1; } or plan skip_all => 'Test::Memory::Cycle not present'; { package MyApp::Cycles; use Dancer2; set auto_page => 1; set serializer => 'JSON'; get '/**' => sub { return { hello => 'world' }; }; } my $app = MyApp::Cycles->to_app; my $runner = Dancer2->runner; Test::Memory::Cycle::memory_cycle_ok( $runner, "runner has no memory cycles" ); Test::Memory::Cycle::memory_cycle_ok( $app, "App has no memory cycles" ); done_testing(); tokens.tt100644001751001751 27313171470520 16150 0ustar00jasonjason000000000000Dancer2-0.205002/t/viewsperl_version: [% perl_version %] dancer_version: [% dancer_version %] settings.foo: [% settings.foo %] params.foo: [% params.foo %] session.foo [% session.foo %] vars.foo: [% vars.foo %] public000755001751001751 013171470520 14273 5ustar00jasonjason000000000000Dancer2-0.205002/tfile.txt100644001751001751 2613171470520 16051 0ustar00jasonjason000000000000Dancer2-0.205002/t/publicthis is a public file send_file.t100644001751001751 733313171470520 16062 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use utf8; use Encode 'encode_utf8'; use Test::More; use Plack::Test; use HTTP::Request::Common; use File::Temp; use File::Spec; use Ref::Util qw; { package StaticContent; use Dancer2; use Encode 'encode_utf8'; set views => 't/corpus/static'; set public_dir => 't/corpus/static'; get '/' => sub { send_file 'index.html'; }; prefix '/some' => sub { get '/image' => sub { send_file '1x1.png'; return "send_file returns; this content is ignored"; }; }; get '/stringref' => sub { my $string = encode_utf8("This is əɯosəʍɐ an test string"); send_file( \$string ); }; get '/filehandle' => sub { open my $fh, "<:raw", __FILE__; send_file( $fh, content_type => 'text/plain', charset => 'utf-8' ); }; get '/check_content_type' => sub { my $temp = File::Temp->new(); print $temp "hello"; close $temp; send_file($temp->filename, content_type => 'image/png', system_path => 1); }; get '/no_streaming' => sub { my $file = File::Spec->rel2abs(__FILE__); send_file( $file, system_path => 1, streaming => 0 ); }; get '/options_streaming' => sub { my $file = File::Spec->rel2abs(__FILE__); send_file( $file, system_path => 1, streaming => 1 ); }; } my $app = StaticContent->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; subtest "Text content" => sub { my $r = $cb->( GET '/' ); is( $r->code, 200, 'send_file sets the status to 200' ); my $charset = $r->headers->content_type_charset; is( $charset, 'UTF-8', 'Text content type has UTF-8 charset' ); my $test_string = encode_utf8('áéíóú'); like( $r->content, qr{$test_string}, 'Text content contains UTF-8 characters', ); }; subtest "Binary content" => sub { my $r = $cb->( GET '/some/image' ); is( $r->code, 200, 'send_file sets the status to 200 (binary content)' ); unlike( $r->content, qr/send_file returns/, "send_file returns immediately with content"); is( $r->header( 'Content-Type' ), 'image/png', 'correct content_type in response' ); }; subtest "string refs" => sub { my $r = $cb->( GET '/stringref' ); is( $r->code, 200, 'send_file set status to 200 (string ref)'); like( $r->content, qr{test string}, 'stringref content' ); }; subtest "filehandles" => sub { my $r = $cb->( GET '/filehandle' ); is( $r->code, 200, 'send_file set status to 200 (filehandle)'); is( $r->content_type, 'text/plain', 'expected content_type'); is( $r->content_type_charset, 'UTF-8', 'expected charset'); like( $r->content, qr{package StaticContent}, 'filehandle content' ); }; subtest "no streaming" => sub { my $r = $cb->( GET '/no_streaming' ); is( $r->code, 200, 'send_file set status to 200 (no streaming)'); like( $r->content, qr{package StaticContent}, 'no streaming - content' ); }; subtest "options streaming" => sub { my $r = $cb->( GET '/options_streaming' ); is( $r->code, 200, 'send_file set status to 200 (options streaming)'); like( $r->content, qr{package StaticContent}, 'options streaming - content' ); }; subtest 'send_file returns correct content type' => sub { my $r = $cb->( GET '/check_content_type' ); ok($r->is_success, 'send_file returns success'); is($r->content_type, 'image/png', 'send_file returns correct content_type'); }; }; done_testing; issues000755001751001751 013171470520 14330 5ustar00jasonjason000000000000Dancer2-0.205002/tgh-799.t100644001751001751 313413171470520 15602 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More tests => 1; use Test::Fatal; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set log => 'core'; set engines => { logger => { Capture => { log_format => '%{x-test}h %i' } }, }; set logger => 'Capture'; get '/' => sub { my $req = app->request; ::isa_ok( $req, 'Dancer2::Core::Request' ); my $logger = app->engine('logger'); ::isa_ok( $logger, 'Dancer2::Logger::Capture' ); ::can_ok( $logger, 'format_message' ); my $trap = $logger->trapper; ::isa_ok( $trap, 'Dancer2::Logger::Capture::Trap' ); my $msg = $trap->read; ::is_deeply( $msg, [ { level => 'core', message => 'looking for get /', formatted => "- 1\n", }, { level => 'core', message => 'Entering hook core.app.before_request', formatted => "- 1\n", }, ], 'Messages logged successfully', ); ::can_ok( $logger, 'format_message' ); my $fmt_str = $logger->format_message( $msg->[0]{'debug'}, $msg->[0]{'message'} ); ::is( $fmt_str, "- 1\n", 'Correct formatted message created' ); return; }; } my $test = Plack::Test->create( App->to_app ); subtest 'Logger can access request' => sub { my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); }; gh-936.t100644001751001751 122013171470520 15565 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse warnings; use strict; use Test::More; use Plack::Test; use HTTP::Request::Common; { package TestApp; use Dancer2; set views => 't/issues/gh-936/views'; set error_template => 'error'; get '/does-not-exist' => sub { send_error "not found", 404; }; } my $test = Plack::Test->create( Dancer2->psgi_app ); for my $path ( qw{does-not-exist anywhere} ) { subtest "$path" => sub { my $res = $test->request( GET "/$path" ); is $res->code, 404, 'status is 404'; like $res->content, qr{CUSTOM ERROR TEMPLATE GOES HERE}, 'Error message looks good'; }; } done_testing(); gh-794.t100644001751001751 76313171470520 15562 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set serializer => 'JSON'; post '/' => sub { request->data }; } my $test = Plack::Test->create( App->to_app ); is( $test->request( POST '/', Content => '{"foo":42}' )->content, '{"foo":42}', 'Correct JSON content in POST', ); is( $test->request( POST '/', Content => 'invalid' )->code, 500, 'Failed to decode invalid content', ); gh-762.t100644001751001751 176013171470520 15573 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse Test::More; use Plack::Test; use HTTP::Request::Common; { package FourOhFour; use Dancer2; set views => 't/issues/gh-762/views'; get '/error' => sub { send_error "oh my", 404; }; } my $fourohfour_app = FourOhFour->to_app; my $fourohfour_test = Plack::Test->create($fourohfour_app); subtest "/error" => sub { my $res = $fourohfour_test->request( GET '/error' ); is $res->code, 404, 'send_error sets the status to 404'; like $res->content, qr{Template selected}, 'Error message looks good'; like $res->content, qr{message: oh my}; like $res->content, qr{status: 404}; }; subtest 'FourOhFour with views template' => sub { my $path = "/middle/of/nowhere"; my $res = $fourohfour_test->request( GET $path ); is $res->code, 404, 'unknown route => 404'; like $res->content, qr{Template selected}, 'Error message looks good'; like $res->content, qr{message: $path}; like $res->content, qr{status: 404}; }; done_testing(); gh-811.t100644001751001751 245113171470520 15564 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; eval { require Dancer2::Session::Cookie; 1 } or plan skip_all => 'Dancer2::Session::Cookie probably missing.'; { package App; use Dancer2; set engines => { session => { Cookie => { secret_key => 'you cannot buy happiness' } } }; set session => 'Cookie'; get '/set' => sub { session foo => 'bar'; redirect '/get'; }; get '/get' => sub { my $data = session->data; return to_json $data; }; } my $test = Plack::Test->create( App->to_app ); my $jar = HTTP::Cookies->new; my $url = 'http://localhost'; my $redir; subtest 'Creating a session' => sub { my $res = $test->request( GET "$url/set" ); ok( $res->is_redirect, 'Request causes redirect' ); ($redir) = $res->header('Location'); is( $redir, "$url/get", 'Redirects to correct url' ); $jar->extract_cookies($res); ok( $jar->as_string, 'Received a session cookie' ); }; subtest 'Retrieving a session' => sub { my $req = GET $redir; $jar->add_cookie_header($req); my $res = $test->request($req); ok( $res->is_success, 'Successful request' ); is( $res->content, '{"foo":"bar"}', 'Correct response' ); }; done_testing; gh-944.t100644001751001751 202113171470520 15564 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; { package RouteContentTest; ## no critic use Dancer2; set serializer => 'JSON'; hook before => sub { return if request->path eq '/content'; response->content({ foo => 'bar' }); response->halt; }; get '/' => sub {1}; get '/content' => sub { response->content({ foo => 'bar' }); return 'this is ignored'; }; } my $test = Plack::Test->create( RouteContentTest->to_app ); subtest "response set in before hook" => sub { my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->content, '{"foo":"bar"}', 'Correct content' ); }; subtest "response content set in route" => sub { my $res = $test->request( GET '/content' ); ok( $res->is_success, 'Successful request' ); isnt( $res->content, 'this is ignored', 'route return value ignored' ); is( $res->content, '{"foo":"bar"}', 'Correct content' ); }; done_testing(); gh-797.t100644001751001751 237613171470520 15607 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use JSON::MaybeXS; { package App; use Dancer2; set serializer => 'JSON'; post '/' => sub { my $post_params = params('body'); # should work even with empty post body my $foo = $post_params->{'foo'}; return { foo => $foo }; }; } my $test = Plack::Test->create( App->to_app ); my %headers; subtest 'Basic response failing' => sub { TODO: { local $TODO = '500 when deserializing bad input'; my $res = $test->request( POST '/', { foo => 'bar' }, %headers ); is( $res->code, 500, '[POST /] Failed when sending regular params' ); } }; subtest 'Basic response' => sub { my $res = $test->request( POST '/', %headers, Content => encode_json { foo => 'bar' } ); is( $res->code, 200, '[POST /] Correct response code' ); my $response_data = decode_json( $res->decoded_content ); is($response_data->{foo}, 'bar', "[POST /] Correct response data"); }; subtest 'Empty POST' => sub { my $res = $test->request( POST '/', {}, %headers ); is( $res->code, 200, '[POST /] Correct response code with empty post body', ); }; done_testing(); gh-730.t100644001751001751 255313171470520 15567 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More tests => 3; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { request->is_behind_proxy }; } my $app = App->to_app; isa_ok( $app, 'CODE' ); my $test = Plack::Test->create($app); subtest 'Runner config' => sub { plan tests => 5; is( Dancer2->runner->config->{'behind_proxy'}, 0, 'No default behind_proxy', ); is( scalar @{ Dancer2->runner->apps }, 1, 'Single app registered', ); isa_ok( Dancer2->runner->apps->[0], 'Dancer2::Core::App', 'Correct app registered', ); is( Dancer2->runner->apps->[0]->setting('behind_proxy'), 0, 'behind_proxy not defined by default in an app', ); Dancer2->runner->apps->[0]->config->{'behind_proxy'} = 1; is( Dancer2->runner->apps->[0]->setting('behind_proxy'), 1, 'Set behind_proxy locally in the app to one', ); }; subtest 'Using App-level settings' => sub { plan tests => 3; is( Dancer2->runner->config->{'behind_proxy'}, 0, 'Runner\'s behind_proxy is still the default', ); my $res = $test->request( GET '/' ); is( $res->code, 200, '[GET /] Correct code' ); is( $res->content, '1', '[GET /] Local value achieved' ); }; gh-931.t100644001751001751 307613171470520 15573 0ustar00jasonjason000000000000Dancer2-0.205002/t/issues# this test checks the order of parameters precedence # we run a few request to a route # first we check that the route parameters have precedence # then we check that the body parameters have the next # and finally, when others aren't available, query parameters use strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; { package App; ## no critic use Dancer2; sub query_ok { ::is( params('query')->{'var'}, 'QueryVar', 'Query variable exists', ); } sub body_ok { ::is( params('body')->{'var'}, 'BodyVar', 'Body variable exists', ); } sub route_ok { ::is( params('route')->{'var'}, 'RouteVar', 'Route variable exists', ); } post '/:var' => sub { query_ok(); body_ok(); route_ok(); ::is( params->{'var'}, 'RouteVar', 'Route variable wins', ); }; post '/' => sub { query_ok(); body_ok(); ::is( params->{'var'}, 'BodyVar', 'Body variable wins', ); }; } my $test = Plack::Test->create( App->to_app ); subtest 'Route takes precedence over all other parameters' => sub { $test->request( POST '/RouteVar?var=QueryVar', [ var => 'BodyVar' ] ); }; subtest 'When route parameters not available, POST takes precedence' => sub { $test->request( POST '/?var=QueryVar', [ var => 'BodyVar' ] ); }; done_testing(); gh-634.t100644001751001751 446613171470520 15577 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More tests=> 3; use File::Temp qw/tempdir/; use File::Spec; my $log_dir = tempdir( CLEANUP => 1 ); { package LogDirSpecified; use Dancer2; set engines => { logger => { File => { log_dir => $log_dir, file_name => 'test_log.log', } } }; set logger => 'file'; } { package NonExistLogDirSpecified; use Dancer2; set engines => { logger => { File => { log_dir => "$log_dir/notexist", file_name => 'test_log.log', } } }; set logger => 'file'; } { package LogDirNotSpecified; use Dancer2; set logger => 'file'; } my $check_cb = sub { my ( $app, $dir, $file ) = @_; my $logger = $app->logger_engine; isa_ok( $logger, 'Dancer2::Logger::File' ); is( $logger->environment, $app->environment, 'Logger got correct environment', ); is( $logger->location, $app->config_location, 'Logger got correct location', ); is( $logger->log_dir, $dir, 'Logger got correct log directory', ); is( $logger->file_name, $file, 'Logger got correct filename', ); is( $logger->log_file, File::Spec->catfile( $dir, $file ), 'Logger got correct log file', ); }; subtest 'test Logger::File with log_dir specified' => sub { plan tests => 6; my $app = [ grep { $_->name eq 'LogDirSpecified' } @{ Dancer2->runner->apps } ]->[0]; $check_cb->( $app, $log_dir, 'test_log.log' ); }; subtest 'test Logger::File with log_dir NOT specified' => sub { plan tests => 6; my $app = [ grep { $_->name eq 'LogDirNotSpecified' } @{ Dancer2->runner->apps } ]->[0]; $check_cb->( $app, File::Spec->catdir( $app->config_location, 'logs' ), $app->environment . '.log', ); }; subtest 'test Logger::File with non-existent log_dir specified' => sub { plan tests => 6; my $app = [ grep { $_->name eq 'NonExistLogDirSpecified'} @{ Dancer2->runner->apps } ]->[0]; my $logger = $app->logger_engine; $check_cb->( $app, "$log_dir/notexist", 'test_log.log', ); }; gh-723.t100644001751001751 225513171470520 15570 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More tests => 4; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub {'OK'}; } { package App::Extended; use Dancer2; prefix '/test'; get '/' => sub {'Also OK'}; post '/' => sub { my $params = params; ::isa_ok( $params, 'HASH' ); ::is( $params->{'foo'}, 'bar', 'Got params' ); return $params->{'foo'}; }; } my $app = Dancer2->psgi_app; isa_ok( $app, 'CODE' ); my $test = Plack::Test->create($app); subtest 'GET /' => sub { plan tests => 2; my $res = $test->request( GET '/' ); is( $res->code, 200, 'Correct code' ); is( $res->content, 'OK', 'Correct content' ); }; subtest 'GET /test/' => sub { plan tests => 2; my $res = $test->request( GET '/test/' ); is( $res->code, 200, 'Correct code' ); is( $res->content, 'Also OK', 'Correct content' ); }; subtest 'Missing POST params' => sub { plan tests => 4; my $res = $test->request( POST '/test/', { foo => 'bar' }, ); is( $res->code, 200, 'Correct code' ); is( $res->content, 'bar', 'Correct content' ); }; gh-596.t100644001751001751 66113171470520 15557 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; BEGIN { $ENV{'DANCER_NO_SERVER_TOKENS'} = 'foo' } { package App; use Dancer2; get '/' => sub { config->{'no_server_tokens'} }; } my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful' ); is( $res->content, 'foo', 'Correct server tokens configuration' ); plugin2000755001751001751 013171470520 14375 5ustar00jasonjason000000000000Dancer2-0.205002/tbasic.t100644001751001751 120113171470520 15775 0ustar00jasonjason000000000000Dancer2-0.205002/t/plugin2use strict; use warnings; use Test::More tests => 6; use Plack::Test; use HTTP::Request::Common; use lib 't/lib'; use poc; my $test = Plack::Test->create( poc->to_app ); note "poc root"; { my $res = $test->request( GET '/' ); ok $res->is_success; my $content = $res->content; like $content, qr/added by plugin/; like $content, qr/something:1/, 'config parameters are read'; like $content, qr/Bar loaded/, 'Plugin Bar has been loaded'; like $content, qr/bazbazbaz/, 'Foo has a copy of Bar'; } note "poc truncate"; { my $res = $test->request( GET '/truncate' ); like $res->content, qr'helladd'; } hooks.t100644001751001751 340213171470520 16044 0ustar00jasonjason000000000000Dancer2-0.205002/t/plugin2use strict; use warnings; use Test::More tests => 3; use Plack::Test; use HTTP::Request::Common; { package Dancer2::Plugin::FooDetector; use Dancer2::Plugin; plugin_hooks 'foo'; sub BUILD { my $plugin = shift; $plugin->app->add_hook( Dancer2::Core::Hook->new( name => 'after', code => sub { $plugin->app->execute_hook( 'plugin.foodetector.foo' ) if $_[0]->content =~ /foo/; } ) ); } } { package PoC; use Dancer2; use Dancer2::Plugin::FooDetector; my $hooked = 'nope'; my $counter = 0; hook 'plugin.foodetector.foo' => sub { $counter++; $hooked = 'hooked'; }; get '/' => sub { "saying foo triggers the hook" }; get 'meh' => sub { 'meh' }; get '/hooked' => sub { $hooked }; get '/counter' => sub { $counter }; } my $test = Plack::Test->create( PoC->to_app ); subtest 'initial state' => sub { ok $test->request( GET '/meh' )->is_success; my $res = $test->request( GET '/hooked' ); ok $res->is_success; is $res->content, 'nope'; is $test->request( GET '/counter' )->content, '0'; }; subtest 'trigger hook' => sub { ok $test->request( GET '/' )->is_success; my $res = $test->request( GET '/hooked' ); ok $res->is_success; is $res->content, 'hooked'; is $test->request( GET '/counter' )->content, '1'; }; # GH #1018 - ensure hooks are called the correct number of times subtest 'execute hook counting' => sub { ok $test->request( GET '/' )->is_success; my $res = $test->request( GET '/hooked' ); ok $res->is_success; is $res->content, 'hooked'; is $test->request( GET '/counter' )->content, '2'; }; Dancer2000755001751001751 013171470520 14576 5ustar00jasonjason000000000000Dancer2-0.205002/libCLI.pm100644001751001751 103113171470520 15676 0ustar00jasonjason000000000000Dancer2-0.205002/lib/Dancer2package Dancer2::CLI; # ABSTRACT: Dancer2 cli application $Dancer2::CLI::VERSION = '0.205002'; use strict; use warnings; use App::Cmd::Setup -app; 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::CLI - Dancer2 cli application =head1 VERSION version 0.205002 =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2017 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut shared_engines.t100644001751001751 212213171470520 16315 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; { package App; # call stuff before next use() statement BEGIN { use Dancer2; set session => 'Simple'; engine('session')->{'__marker__'} = 1; } use lib '.'; use t::lib::Foo with => { session => engine('session') }; get '/main' => sub { session( 'test' => 42 ); }; } my $jar = HTTP::Cookies->new; my $url = 'http://localhost'; { my $test = Plack::Test->create( App->to_app ); my $res = $test->request( GET "$url/main" ); like $res->content, qr{42}, "session is set in main"; $jar->extract_cookies($res); ok( $jar->as_string, 'Got cookie' ); } { my $test = Plack::Test->create( t::lib::Foo->to_app ); my $req = GET "$url/in_foo"; $jar->add_cookie_header($req); my $res = $test->request($req); like $res->content, qr{42}, "session is set in foo"; } my $engine = t::lib::Foo->dsl->engine('session'); is $engine->{__marker__}, 1, "the session engine in subapp is the same"; done_testing; logger_console.t100644001751001751 264413171470520 16351 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Capture::Tiny 0.12 'capture_stderr'; use Dancer2::Logger::Console; my $file = __FILE__; my $l = Dancer2::Logger::Console->new( app_name => 'test', log_level => 'core' ); for my $level (qw{core debug info warning error}) { my $stderr = capture_stderr { $l->$level("$level") }; # We are dealing directly with the logger, not through the DSL. # Skipping 5 stack frames is likely to point to somewhere outside # this test; however Capture::Tiny adds in several call frames # (see below) to capture the output, giving a reasonable caller # to test for like $stderr, qr{$level in \Q$file\E l[.] 15}, "$level message sent"; } done_testing; __END__ # Stack frames involved where Role::Logger executes caller(5): # Dancer2::Core::Role::Logger::format_message(Dancer2::Logger::Console=HASH(0x7f8e41029c60), "error", "error") called at lib/Dancer2/Logger/Console.pm line 10 # Dancer2::Logger::Console::log(Dancer2::Logger::Console=HASH(0x7f8e41029c60), "error", "error") called at lib/Dancer2/Core/Role/Logger.pm line 183 # Dancer2::Core::Role::Logger::error(Dancer2::Logger::Console=HASH(0x7f8e41029c60), "error") called at t/logger_console.t line 12 # main::__ANON__() called at Capture/Tiny.pm line 369 # eval {...} called at Capture/Tiny.pm line 369 # Capture::Tiny::_capture_tee(0, 1, 0, 0, CODE(0x7f8e418181e0)) called at t/logger_console.t line 12 static_content.t100644001751001751 115513171470520 16365 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use utf8; use Test::More; use Plack::Test; use HTTP::Request::Common; { package PublicContent; use Dancer2; set public_dir => 't/corpus/static'; get '/' => sub { return 'Welcome Home' }; } my $test = Plack::Test->create( PublicContent->to_app ); subtest 'public content' => sub { my $res = $test->request( GET '/1x1.png' ); is $res->code, 200, "200 response"; my $last_modified = $res->header('Last-Modified'); $res = $test->request( GET '/1x1.png', 'If-Modified-Since' => $last_modified ); is $res->code, 304, "304 response"; }; done_testing(); request_upload.t100644001751001751 1416113171470520 16421 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings FATAL => 'all'; use Test::More; use Test::Fatal; use Dancer2::Core::Request; use Carp; use File::Temp 0.22; use File::Basename qw/dirname basename/; use File::Spec; use Encode qw(encode_utf8); diag "If you want extra speed, install URL::Encode::XS" if !$Dancer2::Core::Request::XS_URL_DECODE; diag "If you want extra speed, install CGI::Deurl::XS" if !$Dancer2::Core::Request::XS_PARSE_QUERY_STRING; sub test_path { my ( $file, $dir ) = @_; is dirname($file), $dir, "dir of $file is $dir"; } sub run_test { my $filename = "some_\x{1A9}_file.txt"; my $content = qq{------BOUNDARY Content-Disposition: form-data; name="test_upload_file"; filename="$filename" Content-Type: text/plain SHOGUN ------BOUNDARY Content-Disposition: form-data; name="test_upload_file"; filename="yappo2.txt" Content-Type: text/plain SHOGUN2 ------BOUNDARY Content-Disposition: form-data; name="test_upload_file3"; filename="yappo3.txt" Content-Type: text/plain SHOGUN3 ------BOUNDARY Content-Disposition: form-data; name="test_upload_file4"; filename="yappo4.txt" Content-Type: text/plain SHOGUN4 ------BOUNDARY Content-Disposition: form-data; name="test_upload_file4"; filename="yappo5.txt" Content-Type: text/plain SHOGUN4 ------BOUNDARY Content-Disposition: form-data; name="test_upload_file6"; filename="yappo6.txt" Content-Type: text/plain SHOGUN6 ------BOUNDARY-- }; $content =~ s/\r\n/\n/g; $content =~ s/\n/\r\n/g; $content = encode_utf8($content); do { open my $in, '<', \$content; my $req = Dancer2::Core::Request->new( env => { 'psgi.input' => $in, CONTENT_LENGTH => length($content), CONTENT_TYPE => 'multipart/form-data; boundary=----BOUNDARY', REQUEST_METHOD => 'POST', SCRIPT_NAME => '/', SERVER_PORT => 80, } ); my @undef = $req->upload('undef'); is @undef, 0, 'non-existent upload as array is empty'; my $undef = $req->upload('undef'); is $undef, undef, '... and non-existent upload as scalar is undef'; my @uploads = $req->upload('test_upload_file'); like $uploads[0]->content, qr|^SHOGUN|, "content for first upload is ok, via 'upload'"; like $uploads[1]->content, qr|^SHOGUN|, "... content for second as well"; is $req->uploads->{'test_upload_file4'}[0]->content, 'SHOGUN4', "... content for other also good"; note "headers"; is_deeply $uploads[0]->headers, { 'Content-Disposition' => qq[form-data; name="test_upload_file"; filename="$filename"], 'Content-Type' => 'text/plain', }; note "type"; is $uploads[0]->type, 'text/plain'; my $test_upload_file3 = $req->upload('test_upload_file3'); is $test_upload_file3->content, 'SHOGUN3', "content for upload #3 as a scalar is good, via req->upload"; my @test_upload_file6 = $req->upload('test_upload_file6'); is $test_upload_file6[0]->content, 'SHOGUN6', "content for upload #6 is good"; is $test_upload_file6[0]->content(':raw'), 'SHOGUN6'; my $upload = $req->upload('test_upload_file6'); isa_ok $upload, 'Dancer2::Core::Request::Upload'; is $upload->filename, 'yappo6.txt', 'filename is ok'; ok $upload->file_handle, 'file handle is defined'; is $req->params->{'test_upload_file6'}, 'yappo6.txt', "filename is accessible via params"; # copy_to, link_to my $dest_dir = File::Temp::tempdir( CLEANUP => 1, TMPDIR => 1 ); my $dest_file = File::Spec->catfile( $dest_dir, $upload->basename ); $upload->copy_to($dest_file); ok( ( -f $dest_file ), "file '$dest_file' has been copied" ); my $dest_file_link = File::Spec->catfile( $dest_dir, "hardlink" ); $upload->link_to($dest_file_link); ok( ( -f $dest_file_link ), "hardlink '$dest_file_link' has been created" ); # make sure cleanup is performed when the HTTP::Body object is purged my $file = $upload->tempname; ok( ( -f $file ), 'temp file exists while HTTP::Body lives' ); undef $req->{_http_body}; SKIP: { skip "Win32 can't remove file/link while open, deadlock with HTTP::Body", 1 if ( $^O eq 'MSWin32' ); ok( ( !-f $file ), 'temp file is removed when HTTP::Body object dies' ); } note "testing failing open for tempfile"; { # mocking open_file to make it fail my $upload_file_coderef; { no strict 'refs'; $upload_file_coderef = *{"Dancer2::Core::Request::Upload::open_file"}{CODE}; no warnings 'redefine'; *{"Dancer2::Core::Request::Upload::open_file"} = sub { croak "Can't open mocked-tempfile using mode '<'"; }; } $upload->{_fh} = undef; like( exception { $upload->file_handle }, qr{Can't open.* using mode '<'}, ); # unmock open_file { no strict 'refs'; no warnings 'redefine'; *{"Dancer2::Core::Request::Upload::open_file"} = $upload_file_coderef; } } unlink($file) if ( $^O eq 'MSWin32' ); }; } note "Run test with XS_URL_DECODE" if $Dancer2::Core::Request::XS_URL_DECODE; note "Run test with XS_PARSE_QUERY_STRING" if $Dancer2::Core::Request::XS_PARSE_QUERY_STRING; run_test(); if ($Dancer2::Core::Request::XS_PARSE_QUERY_STRING) { note "Run test without XS_PARSE_QUERY_STRING"; $Dancer2::Core::Request::XS_PARSE_QUERY_STRING = 0; $Dancer2::Core::Request::_count = 0; run_test(); } if ($Dancer2::Core::Request::XS_URL_DECODE) { note "Run test without XS_URL_DECODE"; $Dancer2::Core::Request::XS_URL_DECODE = 0; $Dancer2::Core::Request::_count = 0; run_test(); } done_testing; charset_server.t100644001751001751 241213171470520 16360 0ustar00jasonjason000000000000Dancer2-0.205002/tuse Test::More; use strict; use warnings; use Encode; use utf8; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package App; use Dancer2; get '/name/:name' => sub { "Your name: " . params->{name}; }; post '/name' => sub { "Your name: " . params->{name}; }; get '/unicode' => sub { "cyrillic shcha \x{0429}",; }; get '/symbols' => sub { '⚒ ⚓ ⚔ ⚕ ⚖ ⚗ ⚘ ⚙'; }; set charset => 'utf-8'; } my $app = Dancer2->psgi_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $res = $cb->( POST "/name", [ name => 'vasya'] ); is $res->content_type, 'text/html'; ok $res->content_type_charset ; # we always have charset if the setting is set is $res->content, 'Your name: vasya'; $res = $cb->( GET "/unicode" ); is $res->content_type, 'text/html'; is $res->content_type_charset, 'UTF-8'; is $res->content, Encode::encode( 'utf-8', "cyrillic shcha \x{0429}" ); $res = $cb->( GET "/symbols" ); is $res->content_type, 'text/html'; is $res->content_type_charset, 'UTF-8'; is $res->content, Encode::encode( 'utf-8', "⚒ ⚓ ⚔ ⚕ ⚖ ⚗ ⚘ ⚙" ); }; done_testing(); session_config.t100644001751001751 400713171470520 16353 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; { package App; use Dancer2; setting( engines => { session => { Simple => { cookie_name => 'dancer.sid', cookie_path => '/foo', cookie_duration => '1 hour', is_http_only => 0, # will not show up in cookie }, }, } ); setting( session => 'Simple' ); get '/has_session' => sub { return app->has_session; }; get '/foo/set_session/*' => sub { my ($name) = splat; session name => $name; }; get '/foo/read_session' => sub { my $name = session('name') || ''; "name='$name'"; }; get '/foo/destroy_session' => sub { my $name = session('name') || ''; app->destroy_session; return "destroyed='$name'"; }; } my $test = Plack::Test->create( App->to_app ); my $url = 'http://localhost'; my $jar = HTTP::Cookies->new; subtest 'Set session' => sub { my $res = $test->request( GET "$url/foo/set_session/larry" ); ok( $res->is_success, '/foo/set_session/larry' ); $jar->extract_cookies($res); ok( $jar->as_string, 'session cookie set' ); my ( $expires, $domain, $path, $opts ); my $cookie = $jar->scan( sub { ( $expires, $domain, $path, $opts ) = @_[ 8, 4, 3 ]; } ); my $httponly = $opts->{'HttpOnly'}; ok $expires - time > 3540, "cookie expiration is in future"; is $domain, 'localhost.local', "cookie domain set"; is $path, '/foo', "cookie path set"; is $httponly, undef, "cookie has not set HttpOnly"; # read value back }; subtest 'Read session' => sub { my $req = GET "$url/foo/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/foo/read_session"; like $res->content, qr/name='larry'/, "session value looks good"; }; done_testing; session_object.t100644001751001751 217713171470520 16362 0ustar00jasonjason000000000000Dancer2-0.205002/t# session_object.t use strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::Core::Session; use Dancer2::Session::Simple; my $ENGINE = Dancer2::Session::Simple->new; my $CPRNG_AVAIL = eval { require Math::Random::ISAAC::XS; 1; } && eval { require Crypt::URandom; 1; }; note $CPRNG_AVAIL ? "Crypto strength tokens" : "Default strength tokens"; subtest 'session attributes' => sub { my $s1 = $ENGINE->create; my $id = $s1->id; ok defined($id), 'id is defined'; is(exception { $s1->id("new_$id") }, undef, 'id can be set'); is($s1->id, "new_$id", '... new value found for id'); my $s2 = $ENGINE->create; isnt($s1->id, $s2->id, "IDs are not the same"); }; my $count = 10_000; subtest "$count session IDs and no dups" => sub { my $seen = {}; my $iteration = 0; foreach my $i (1 .. $count) { my $s1 = $ENGINE->create; my $id = $s1->id; if (exists $seen->{$id}) { last; } $seen->{$id} = 1; $iteration++; } is $iteration, $count, "no duplicate ID after $count iterations (done $iteration)"; }; done_testing; parameters.t100644001751001751 2415713171470520 16320 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use utf8; use Test::More; use Plack::Test; use Encode 'encode_utf8'; use HTTP::Request::Common; subtest 'Query parameters' => sub { { package App::Basic; ## no critic use Dancer2; get '/' => sub { my $params = query_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is( $params->get('foo'), 'bar', 'Got single value' ); ::is( $params->get('bar'), 'quux', 'Got single value from multi key', ); ::is_deeply( [ $params->get_all('bar') ], ['baz', 'quux'], 'Got multi value from multi key', ); ::is( $params->get('baz'), 'הלו', 'HMV interface returns encoded values', ); ::is( params->{'baz'}, 'הלו', 'Regular interface returns encoded values' ); }; } my $app = Plack::Test->create( App::Basic->to_app ); my $res = $app->request( GET '/?foo=bar&bar=baz&bar=quux&baz=הלו' ); ok( $res->is_success, 'Successful request' ); }; subtest 'Body parameters' => sub { { package App::Body; ## no critic use Dancer2; post '/' => sub { my $params = body_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is( $params->get('foo'), 'bar', 'Got single value' ); ::is( $params->get('bar'), 'quux', 'Got single value from multi key', ); my $z = [ $params->get_all('bar') ]; ::is_deeply( [ $params->get_all('bar') ], ['baz', 'quux'], 'Got multi value from multi key', ); ::is( $params->get('baz'), 'הלו', 'HMV interface returns encoded values', ); ::is( params->{'baz'}, 'הלו', 'Regular interface returns encoded values' ); }; } my $app = Plack::Test->create( App::Body->to_app ); my $res = $app->request( POST '/', Content => [foo => 'bar', bar => 'baz', bar => 'quux', baz => 'הלו'] ); ok( $res->is_success, 'Successful request' ); }; subtest 'Body parameters with serialized data' => sub { { package App::Body::JSON; ## no critic use Dancer2; set serializer => 'JSON'; post '/' => sub { my $params = body_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is( $params->get('foo'), 'bar', 'Got single value' ); ::is( $params->get('bar'), 'quux', 'Got single value from multi key', ); my $z = [ $params->get_all('bar') ]; ::is_deeply( [ $params->get_all('bar') ], ['baz', 'quux'], 'Got multi value from multi key', ); ::is( $params->get('baz'), 'הלו', 'HMV interface returns encoded values', ); ::is( params->{'baz'}, 'הלו', 'Regular interface returns encoded values' ); return { ok => 1 }; }; } my $app = Plack::Test->create( App::Body::JSON->to_app ); my $baz = encode_utf8('הלו'); my $res = $app->request( POST '/', Content => qq{{"foo":"bar","bar":["baz","quux"],"baz":"$baz"}} ); ok( $res->is_success, 'Successful request' ); }; subtest 'Route parameters' => sub { { package App::Route; ## no critic use Dancer2; get '/:foo' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is( $params->get('foo'), 'bar', 'Got keyed value' ); }; get '/:name/:value' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword returns Hash::MultiValue object', ); ::is( $params->get('name'), 'foo', 'Got first value' ); ::is( $params->get('value'), 'הלו', 'Got second value' ); ::is( params->{'value'}, 'הלו', 'Regular interface returns encoded values' ); }; } my $app = Plack::Test->create( App::Route->to_app ); { my $res = $app->request( GET '/bar' ); ok( $res->is_success, 'Successful request' ); } { my $res = $app->request( GET '/foo/הלו' ); ok( $res->is_success, 'Successful request' ); } }; subtest 'Splat and megasplat route parameters' => sub { { package App::Route::Splat; ## no critic use Dancer2; get '/*' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], [ 'foo' ], 'Got splat values', ); }; get '/*/*' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword returns Hash::MultiValue object', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], [ qw ], 'Got splat values', ); }; # /foo/bar/baz/quux/quuks get '/*/*/*/**' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword returns Hash::MultiValue object', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], [ qw, [ qw ] ], 'Got splat values', ); }; # /foo/bar/baz get '/*/:foo/**' => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword returns Hash::MultiValue object', ); ::is( $params->get('foo'), 'bar', 'Correct route parameter' ); ::is_deeply( [ splat ], [ 'foo', ['baz', ''] ], 'Got splat values', ); }; } my $app = Plack::Test->create( App::Route::Splat->to_app ); { my $res = $app->request( GET '/foo' ); ok( $res->is_success, 'Successful request' ); } { my $res = $app->request( GET '/foo/bar' ); ok( $res->is_success, 'Successful request' ); } { my $res = $app->request( GET '/foo/bar/baz/quux/quuks' ); ok( $res->is_success, 'Successful request' ); } { my $res = $app->request( GET '/foo/bar/baz/' ); ok( $res->is_success, 'Successful request' ); } }; subtest 'Captured route parameters' => sub { { package App::Route::Capture; ## no critic use Dancer2; get qr{^/foo/([^/]+)$} => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], ['bar'], 'Correct splat values', ); ::is_deeply( captures(), +{}, 'capture values are empty', ); }; } my $app = Plack::Test->create( App::Route::Capture->to_app ); { my $res = $app->request( GET '/foo/bar' ); ok( $res->is_success, 'Successful request' ); } }; SKIP: { Test::More::skip "named captures not available until 5.10", 1 if !$^V or $^V lt v5.10; subtest 'Named captured route parameters' => sub { { package App::Route::NamedCapture; ## no critic use Dancer2; my $re = '^/bar/(?[^/]+)$'; get qr{$re} => sub { my $params = route_parameters; ::isa_ok( $params, 'Hash::MultiValue', 'parameters keyword', ); ::is_deeply( { %{$params} }, {}, 'All route parameters are empty', ); ::is_deeply( [ splat ], [], 'splat values are empty', ); ::is_deeply( captures(), { baz => 'quux' }, 'Correct capture values', ); }; } my $app = Plack::Test->create( App::Route::NamedCapture->to_app ); { my $res = $app->request( GET '/bar/quux' ); ok( $res->is_success, 'Successful request' ); }; }; }; done_testing(); gh-1098.t100644001751001751 510413171470520 15652 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse Test::More tests => 3; use Test::Fatal; use Dancer2::Core::Error; use Dancer2::Core::Response; use Dancer2::Serializer::JSON; use HTTP::Headers::Fast; use JSON::MaybeXS; subtest 'Core::Error serializer isa tests' => sub { plan tests => 5; is exception { Dancer2::Core::Error->new }, undef, "Error->new lived"; like exception { Dancer2::Core::Error->new(show_errors => []) }, qr/Reference \Q[]\E did not pass type constraint "Bool"/i, "Error->new(show_errors => []) died"; is exception { Dancer2::Core::Error->new(serializer => undef) }, undef, "Error->new(serializer => undef) lived"; is exception { Dancer2::Core::Error->new(serializer => Dancer2::Serializer::JSON->new) }, undef, "Error->new(serializer => Dancer2::Serializer::JSON->new) lived"; like exception { Dancer2::Core::Error->new(serializer => JSON->new) }, qr/ ( requires\sthat\sthe\sreference\sdoes\sDancer2::Core::Role::Serializer | did\snot\spass\stype\sconstraint ) /x, "Error->new(serializer => JSON->new) died"; }; subtest 'Core::Response headers isa tests' => sub { plan tests => 5; is exception { Dancer2::Core::Response->new }, undef, "Response->new lived"; is exception { Dancer2::Core::Response->new(headers => [Header => 'Content']) }, undef, "Response->new( headers => [ Header => 'Content' ] ) lived"; is exception { Dancer2::Core::Response->new(headers => HTTP::Headers->new) }, undef, "Response->new( headers => HTTP::Headers->new ) lived"; is exception { Dancer2::Core::Response->new(headers => HTTP::Headers::Fast->new) }, undef, "Response->new( headers => HTTP::Headers::Fast->new ) lived"; like exception { Dancer2::Core::Response->new(headers => JSON->new) }, qr/coercion.+failed.+not.+array/i, "Response->new( headers => JSON->new ) died"; }; subtest 'Core::Role::Logger log_level isa tests' => sub { plan tests => 1 + 6 + 1; { package TestLogger; use Moo; with 'Dancer2::Core::Role::Logger'; sub log { } } is exception { TestLogger->new }, undef, "Logger->new lived"; my @levels = qw/core debug info warn warning error/; foreach my $level (@levels) { is exception { TestLogger->new(log_level => $level) }, undef, "Logger->new(log_level => $level) lives"; } like exception { TestLogger->new(log_level => 'BadLevel') }, qr/Value "BadLevel" did not pass type constraint "Enum/, "Logger->new(log_level => 'BadLevel') died"; }; gh-1070.t100644001751001751 74413171470520 15625 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set show_errors => 1; } my $test = Plack::Test->create( App->to_app ); my $content = $test->request( GET '/nonexistent_pathcrazy' )->content; like $content, qr{/nonexistent_path<strong>crazy</strong>}, "Escaped message"; unlike $content, qr{/nonexistent_pathcrazy}, "No unescaped message"; gh-1232.t100644001751001751 236313171470520 15644 0ustar00jasonjason000000000000Dancer2-0.205002/t/issuesuse strict; use warnings; use Test::More tests => 1; use Plack::Test; use Plack::Builder; use Plack::Request; use HTTP::Request::Common; use Encode qw(encode_utf8); { package App; use Dancer2; # default, we're actually overriding this later set serializer => 'JSON'; # for now set logger => 'Capture'; post '/json' => sub { my $p = body_parameters; return [ map +( $_ => $p->get($_) ), sort $p->keys ]; }; } my $psgi = builder { # inline middleware FTW! # Create a Plack::Request object and parse body to tickle #1232 enable sub { my $app = shift; sub { my $req = Plack::Request->new($_[0])->body_parameters; return $app->($_[0]); } }; App->to_app; }; my $test = Plack::Test->create( $psgi ); subtest 'POST request with parameters' => sub { my $characters = encode_utf8("∑∏"); my $res = $test->request( POST "/json", 'Content-Type' => 'application/json', 'Content' => qq!{ "foo": 1, "bar": 2, "baz": "$characters" }! ); is( $res->content, qq!["bar",2,"baz","$characters","foo",1]!, "Body parameters deserialized", ); }; done_testing();skel000755001751001751 013171470520 14612 5ustar00jasonjason000000000000Dancer2-0.205002/share.dancer100644001751001751 013171470520 16115 0ustar00jasonjason000000000000Dancer2-0.205002/share/skelauthor-no-tabs.t100644001751001751 2731013171470520 16230 0ustar00jasonjason000000000000Dancer2-0.205002/t BEGIN { unless ($ENV{AUTHOR_TESTING}) { print qq{1..0 # SKIP these tests are for testing by the author\n}; exit } } use strict; use warnings; # this test was generated with Dist::Zilla::Plugin::Test::NoTabs 0.15 use Test::More 0.88; use Test::NoTabs; my @files = ( 'lib/Dancer2.pm', 'lib/Dancer2/CLI.pm', 'lib/Dancer2/CLI/Command/gen.pm', 'lib/Dancer2/CLI/Command/version.pm', 'lib/Dancer2/Config.pod', 'lib/Dancer2/Cookbook.pod', 'lib/Dancer2/Core.pm', 'lib/Dancer2/Core/App.pm', 'lib/Dancer2/Core/Cookie.pm', 'lib/Dancer2/Core/DSL.pm', 'lib/Dancer2/Core/Dispatcher.pm', 'lib/Dancer2/Core/Error.pm', 'lib/Dancer2/Core/Factory.pm', 'lib/Dancer2/Core/HTTP.pm', 'lib/Dancer2/Core/Hook.pm', 'lib/Dancer2/Core/MIME.pm', 'lib/Dancer2/Core/Request.pm', 'lib/Dancer2/Core/Request/Upload.pm', 'lib/Dancer2/Core/Response.pm', 'lib/Dancer2/Core/Response/Delayed.pm', 'lib/Dancer2/Core/Role/ConfigReader.pm', 'lib/Dancer2/Core/Role/DSL.pm', 'lib/Dancer2/Core/Role/Engine.pm', 'lib/Dancer2/Core/Role/Handler.pm', 'lib/Dancer2/Core/Role/HasLocation.pm', 'lib/Dancer2/Core/Role/Hookable.pm', 'lib/Dancer2/Core/Role/Logger.pm', 'lib/Dancer2/Core/Role/Serializer.pm', 'lib/Dancer2/Core/Role/SessionFactory.pm', 'lib/Dancer2/Core/Role/SessionFactory/File.pm', 'lib/Dancer2/Core/Role/StandardResponses.pm', 'lib/Dancer2/Core/Role/Template.pm', 'lib/Dancer2/Core/Route.pm', 'lib/Dancer2/Core/Runner.pm', 'lib/Dancer2/Core/Session.pm', 'lib/Dancer2/Core/Time.pm', 'lib/Dancer2/Core/Types.pm', 'lib/Dancer2/FileUtils.pm', 'lib/Dancer2/Handler/AutoPage.pm', 'lib/Dancer2/Handler/File.pm', 'lib/Dancer2/Logger/Capture.pm', 'lib/Dancer2/Logger/Capture/Trap.pm', 'lib/Dancer2/Logger/Console.pm', 'lib/Dancer2/Logger/Diag.pm', 'lib/Dancer2/Logger/File.pm', 'lib/Dancer2/Logger/Note.pm', 'lib/Dancer2/Logger/Null.pm', 'lib/Dancer2/Manual.pod', 'lib/Dancer2/Manual/Deployment.pod', 'lib/Dancer2/Manual/Migration.pod', 'lib/Dancer2/Manual/Testing.pod', 'lib/Dancer2/Plugin.pm', 'lib/Dancer2/Plugins.pod', 'lib/Dancer2/Policy.pod', 'lib/Dancer2/Serializer/Dumper.pm', 'lib/Dancer2/Serializer/JSON.pm', 'lib/Dancer2/Serializer/Mutable.pm', 'lib/Dancer2/Serializer/YAML.pm', 'lib/Dancer2/Session/Simple.pm', 'lib/Dancer2/Session/YAML.pm', 'lib/Dancer2/Template/Implementation/ForkedTiny.pm', 'lib/Dancer2/Template/Simple.pm', 'lib/Dancer2/Template/TemplateToolkit.pm', 'lib/Dancer2/Template/Tiny.pm', 'lib/Dancer2/Test.pm', 'lib/Dancer2/Tutorial.pod', 'script/dancer2', 't/00-compile.t', 't/00-report-prereqs.dd', 't/00-report-prereqs.t', 't/app.t', 't/app/t1/bin/app.psgi', 't/app/t1/config.yml', 't/app/t1/lib/App1.pm', 't/app/t1/lib/Sub/App2.pm', 't/app/t2/.dancer', 't/app/t2/config.yml', 't/app/t2/lib/App3.pm', 't/app_alone.t', 't/auto_page.t', 't/caller.t', 't/charset_server.t', 't/classes/Dancer2-Core-Factory/new.t', 't/classes/Dancer2-Core-Hook/new.t', 't/classes/Dancer2-Core-Request/new.t', 't/classes/Dancer2-Core-Request/serializers.t', 't/classes/Dancer2-Core-Response-Delayed/after_hooks.t', 't/classes/Dancer2-Core-Response-Delayed/new.t', 't/classes/Dancer2-Core-Response/new_from.t', 't/classes/Dancer2-Core-Role-Engine/with.t', 't/classes/Dancer2-Core-Role-Handler/with.t', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl', 't/classes/Dancer2-Core-Role-HasLocation/with.t', 't/classes/Dancer2-Core-Role-Serializer/with.t', 't/classes/Dancer2-Core-Role-StandardResponses/with.t', 't/classes/Dancer2-Core-Route/base.t', 't/classes/Dancer2-Core-Route/deprecated_param_keys.t', 't/classes/Dancer2-Core-Route/match.t', 't/classes/Dancer2-Core-Runner/environment.t', 't/classes/Dancer2-Core-Runner/new.t', 't/classes/Dancer2-Core-Runner/psgi_app.t', 't/classes/Dancer2-Core/camelize.t', 't/classes/Dancer2/import-pragmas.t', 't/classes/Dancer2/import.t', 't/config.yml', 't/config/config.yml', 't/config/environments/failure.yml', 't/config/environments/merging.yml', 't/config/environments/production.yml', 't/config/environments/staging.json', 't/config2/config.yml', 't/config2/config_local.yml', 't/config2/environments/lconfig.yml', 't/config2/environments/lconfig_local.yml', 't/config_multiapp.t', 't/config_reader.t', 't/config_settings.t', 't/context-in-before.t', 't/cookie.t', 't/corpus/pretty/505.tt', 't/corpus/pretty/relative.tt', 't/corpus/pretty_public/404.html', 't/corpus/pretty_public/510.html', 't/corpus/static/index.html', 't/custom_dsl.t', 't/dancer-test.t', 't/dancer-test/config.yml', 't/deserialize.t', 't/disp_named_capture.t', 't/dispatcher.t', 't/dsl/any.t', 't/dsl/app.t', 't/dsl/content.t', 't/dsl/delayed.t', 't/dsl/error_template.t', 't/dsl/extend.t', 't/dsl/extend_config/config.yml', 't/dsl/halt.t', 't/dsl/halt_with_param.t', 't/dsl/json.t', 't/dsl/parameters.t', 't/dsl/pass.t', 't/dsl/path.t', 't/dsl/request.t', 't/dsl/route_retvals.t', 't/dsl/send_as.t', 't/dsl/send_file.t', 't/dsl/splat.t', 't/dsl/to_app.t', 't/engine.t', 't/error.t', 't/factory.t', 't/file_utils.t', 't/forward.t', 't/forward_before_hook.t', 't/forward_hmv_params.t', 't/forward_test_tcp.t', 't/hooks.t', 't/http_methods.t', 't/http_status.t', 't/issues/config.yml', 't/issues/gh-1013/gh-1013.t', 't/issues/gh-1013/views/t.tt', 't/issues/gh-1046/config.yml', 't/issues/gh-1046/gh-1046.t', 't/issues/gh-1070.t', 't/issues/gh-1098.t', 't/issues/gh-1216/gh-1216.t', 't/issues/gh-1216/lib/App.pm', 't/issues/gh-1216/lib/App/Extra.pm', 't/issues/gh-1216/lib/Dancer2/Plugin/Null.pm', 't/issues/gh-1226/gh-1226.t', 't/issues/gh-1226/lib/App.pm', 't/issues/gh-1226/lib/App/Extra.pm', 't/issues/gh-1226/lib/Dancer2/Plugin/Test/AccessDSL.pm', 't/issues/gh-1230/gh-1230.t', 't/issues/gh-1230/lib/App.pm', 't/issues/gh-1230/lib/App/Extra.pm', 't/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessDSL.pm', 't/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessPluginDSL.pm', 't/issues/gh-1232.t', 't/issues/gh-596.t', 't/issues/gh-634.t', 't/issues/gh-639/fails/.dancer', 't/issues/gh-639/fails/config.yml', 't/issues/gh-639/fails/issue.t', 't/issues/gh-639/succeeds/.dancer', 't/issues/gh-639/succeeds/config.yml', 't/issues/gh-639/succeeds/issue.t', 't/issues/gh-650/gh-650.t', 't/issues/gh-650/views/environment_setting.tt', 't/issues/gh-723.t', 't/issues/gh-730.t', 't/issues/gh-762.t', 't/issues/gh-762/views/404.tt', 't/issues/gh-794.t', 't/issues/gh-797.t', 't/issues/gh-799.t', 't/issues/gh-811.t', 't/issues/gh-931.t', 't/issues/gh-936.t', 't/issues/gh-936/views/error.tt', 't/issues/gh-944.t', 't/issues/gh-975/config.yml', 't/issues/gh-975/gh-975.t', 't/issues/gh-975/test_public_dir/test.txt', 't/issues/memleak/die_in_hooks.t', 't/issues/vars-in-forward.t', 't/lib/App1.pm', 't/lib/App2.pm', 't/lib/Dancer2/Plugin/Bar.pm', 't/lib/Dancer2/Plugin/DancerPlugin.pm', 't/lib/Dancer2/Plugin/DefineKeywords.pm', 't/lib/Dancer2/Plugin/EmptyPlugin.pm', 't/lib/Dancer2/Plugin/Foo.pm', 't/lib/Dancer2/Plugin/FooPlugin.pm', 't/lib/Dancer2/Plugin/Hookee.pm', 't/lib/Dancer2/Plugin/OnPluginImport.pm', 't/lib/Dancer2/Plugin/PluginWithImport.pm', 't/lib/Dancer2/Plugin/Polite.pm', 't/lib/Dancer2/Session/SimpleNoChangeId.pm', 't/lib/Foo.pm', 't/lib/MyDancerDSL.pm', 't/lib/PoC/Plugin/Polite.pm', 't/lib/SubApp1.pm', 't/lib/SubApp2.pm', 't/lib/TestApp.pm', 't/lib/TestPod.pm', 't/lib/poc.pm', 't/lib/poc2.pm', 't/log_die_before_hook.t', 't/log_levels.t', 't/logger.t', 't/logger_console.t', 't/memory_cycles.t', 't/mime.t', 't/multi_apps.t', 't/multi_apps_forward.t', 't/multiapp_template_hooks.t', 't/named_apps.t', 't/plugin2/basic-2.t', 't/plugin2/basic.t', 't/plugin2/define-keywords.t', 't/plugin2/find_plugin.t', 't/plugin2/from-config.t', 't/plugin2/hooks.t', 't/plugin2/inside-plugin.t', 't/plugin2/keywords-hooks-namespace.t', 't/plugin2/memory_cycles.t', 't/plugin2/no-app-munging.t', 't/plugin2/no-clobbering.t', 't/plugin2/no-config.t', 't/plugin2/with-plugins.t', 't/plugin_import.t', 't/plugin_multiple_apps.t', 't/plugin_register.t', 't/plugin_syntax.t', 't/prepare_app.t', 't/psgi_app.t', 't/psgi_app_forward_and_pass.t', 't/public/file.txt', 't/redirect.t', 't/request.t', 't/request_make_forward_to.t', 't/request_upload.t', 't/response.t', 't/roles/hook.t', 't/route-pod-coverage/route-pod-coverage.t', 't/scope_problems/config.yml', 't/scope_problems/dispatcher_internal_request.t', 't/scope_problems/keywords_before_template_hook.t', 't/scope_problems/session_is_cleared.t', 't/scope_problems/views/500.tt', 't/scope_problems/with_return_dies.t', 't/serializer.t', 't/serializer_json.t', 't/serializer_mutable.t', 't/session_bad_client_cookie.t', 't/session_config.t', 't/session_engines.t', 't/session_forward.t', 't/session_hooks.t', 't/session_hooks_no_change_id.t', 't/session_in_template.t', 't/session_lifecycle.t', 't/session_object.t', 't/shared_engines.t', 't/static_content.t', 't/template.t', 't/template_default_tokens.t', 't/template_ext.t', 't/template_name.t', 't/template_simple.t', 't/template_tiny/01_compile.t', 't/template_tiny/02_trivial.t', 't/template_tiny/03_samples.t', 't/template_tiny/04_compat.t', 't/template_tiny/05_preparse.t', 't/template_tiny/samples/01_hello.tt', 't/template_tiny/samples/01_hello.txt', 't/template_tiny/samples/01_hello.var', 't/template_tiny/samples/02_null.tt', 't/template_tiny/samples/02_null.txt', 't/template_tiny/samples/02_null.var', 't/template_tiny/samples/03_chomp.tt', 't/template_tiny/samples/03_chomp.txt', 't/template_tiny/samples/03_chomp.var', 't/template_tiny/samples/04_nested.tt', 't/template_tiny/samples/04_nested.txt', 't/template_tiny/samples/04_nested.var', 't/template_tiny/samples/05_condition.tt', 't/template_tiny/samples/05_condition.txt', 't/template_tiny/samples/05_condition.var', 't/template_tiny/samples/06_object.tt', 't/template_tiny/samples/06_object.txt', 't/template_tiny/samples/06_object.var', 't/template_tiny/samples/07_nesting.tt', 't/template_tiny/samples/07_nesting.txt', 't/template_tiny/samples/07_nesting.var', 't/template_tiny/samples/08_foreach.tt', 't/template_tiny/samples/08_foreach.txt', 't/template_tiny/samples/08_foreach.var', 't/template_tiny/samples/09_trim.tt', 't/template_tiny/samples/09_trim.txt', 't/template_tiny/samples/09_trim.var', 't/time.t', 't/types.t', 't/uri_for.t', 't/vars.t', 't/views/auto_page.tt', 't/views/beforetemplate.tt', 't/views/folder/page.tt', 't/views/index.tt', 't/views/layouts/main.tt', 't/views/session_in_template.tt', 't/views/template_simple_index.tt', 't/views/tokens.tt' ); notabs_ok($_) foreach @files; done_testing; Core.pm100644001751001751 141213171470520 16162 0ustar00jasonjason000000000000Dancer2-0.205002/lib/Dancer2package Dancer2::Core; # ABSTRACT: Core libraries for Dancer2 2.0 $Dancer2::Core::VERSION = '0.205002'; use strict; use warnings; sub camelize { my ($value) = @_; my $camelized = ''; for my $word ( split /_/, $value ) { $camelized .= ucfirst($word); } return $camelized; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Core - Core libraries for Dancer2 2.0 =head1 VERSION version 0.205002 =head1 FUNCTIONS =head2 camelize Camelize a underscore-separated-string. =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2017 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut Test.pm100644001751001751 6617613171470520 16253 0ustar00jasonjason000000000000Dancer2-0.205002/lib/Dancer2package Dancer2::Test; # ABSTRACT: Useful routines for testing Dancer2 apps $Dancer2::Test::VERSION = '0.205002'; use strict; use warnings; use Carp qw; use Test::More; use Test::Builder; use URI::Escape; use Data::Dumper; use File::Temp; use Ref::Util qw; use parent 'Exporter'; our @EXPORT = qw( dancer_response response_content_is response_content_isnt response_content_is_deeply response_content_like response_content_unlike response_status_is response_status_isnt response_headers_include response_headers_are_deeply response_is_file route_exists route_doesnt_exist is_pod_covered route_pod_coverage ); #dancer1 also has read_logs, response_redirect_location_is #cf. https://github.com/PerlDancer2/Dancer22/issues/25 use Dancer2::Core::Dispatcher; use Dancer2::Core::Request; # singleton to store all the apps my $_dispatcher = Dancer2::Core::Dispatcher->new; # prevent deprecation warnings our $NO_WARN = 0; # can be called with the ($method, $path, $option) triplet, # or can be fed a request object directly, or can be fed # a single string, assumed to be [ GET => $string ] # or can be fed a response (which is passed through without # any modification) sub dancer_response { carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; _find_dancer_apps_for_dispatcher(); # useful for the high-level tests return $_[0] if ref $_[0] eq 'Dancer2::Core::Response'; my ( $request, $env ) = ref $_[0] eq 'Dancer2::Core::Request' ? _build_env_from_request(@_) : _build_request_from_env(@_); # override the set_request so it actually sets our request instead { ## no critic qw(TestingAndDebugging::ProhibitNoWarnings) no warnings qw; *Dancer2::Core::App::set_request = sub { my $self = shift; $self->_set_request( $request ); $_->set_request( $request ) for @{ $self->defined_engines }; }; } # since the response is a PSGI response # we create a Response object which was originally expected my $psgi_response = $_dispatcher->dispatch($env); return Dancer2::Core::Response->new( status => $psgi_response->[0], headers => $psgi_response->[1], content => $psgi_response->[2][0], ); } sub _build_request_from_env { # arguments can be passed as the triplet # or as a arrayref, or as a simple string my ( $method, $path, $options ) = @_ > 1 ? @_ : is_arrayref($_[0]) ? @{ $_[0] } : ( GET => $_[0], {} ); my $env = { %ENV, REQUEST_METHOD => uc($method), PATH_INFO => $path, QUERY_STRING => '', 'psgi.url_scheme' => 'http', SERVER_PROTOCOL => 'HTTP/1.0', SERVER_NAME => 'localhost', SERVER_PORT => 3000, HTTP_HOST => 'localhost', HTTP_USER_AGENT => "Dancer2::Test simulator v " . Dancer2->VERSION, }; if ( defined $options->{params} ) { my @params; while ( my ( $p, $value ) = each %{ $options->{params} } ) { if ( is_arrayref($value) ) { for my $v (@$value) { push @params, uri_escape_utf8($p) . '=' . uri_escape_utf8($v); } } else { push @params, uri_escape_utf8($p) . '=' . uri_escape_utf8($value); } } $env->{QUERY_STRING} = join( '&', @params ); } my $request = Dancer2::Core::Request->new( env => $env ); # body $request->body( $options->{body} ) if exists $options->{body}; # headers if ( $options->{headers} ) { for my $header ( @{ $options->{headers} } ) { my ( $name, $value ) = @{$header}; $request->header( $name => $value ); if ( $name =~ /^cookie$/i ) { $env->{HTTP_COOKIE} = $value; } } } # files if ( $options->{files} ) { for my $file ( @{ $options->{files} } ) { my $headers = $file->{headers}; $headers->{'Content-Type'} ||= 'text/plain'; my $temp = File::Temp->new(); if ( $file->{data} ) { print $temp $file->{data}; close($temp); } else { require File::Copy; File::Copy::copy( $file->{filename}, $temp ); } my $upload = Dancer2::Core::Request::Upload->new( filename => $file->{filename}, size => -s $temp->filename, tempname => $temp->filename, headers => $headers, ); ## keep temp_fh in scope so it doesn't get deleted too early ## But will get deleted by the time the test is finished. $upload->{temp_fh} = $temp; $request->uploads->{ $file->{name} } = $upload; } } # content-type if ( $options->{content_type} ) { $request->content_type( $options->{content_type} ); } return ( $request, $env ); } sub _build_env_from_request { my ($request) = @_; my $env = { REQUEST_METHOD => $request->method, PATH_INFO => $request->path, QUERY_STRING => '', 'psgi.url_scheme' => 'http', SERVER_PROTOCOL => 'HTTP/1.0', SERVER_NAME => 'localhost', SERVER_PORT => 3000, HTTP_HOST => 'localhost', HTTP_USER_AGENT => "Dancer2::Test simulator v" . Dancer2->VERSION, }; # TODO if ( my $params = $request->{_query_params} ) { my @params; while ( my ( $p, $value ) = each %{$params} ) { if ( is_arrayref($value) ) { for my $v (@$value) { push @params, uri_escape_utf8($p) . '=' . uri_escape_utf8($v); } } else { push @params, uri_escape_utf8($p) . '=' . uri_escape_utf8($value); } } $env->{QUERY_STRING} = join( '&', @params ); } # TODO files return ( $request, $env ); } sub response_status_is { my ( $req, $status, $test_name ) = @_; carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; $test_name ||= "response status is $status for " . _req_label($req); my $response = dancer_response($req); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->is_eq( $response->[0], $status, $test_name ); } sub _find_route_match { my ( $request, $env ) = ref $_[0] eq 'Dancer2::Core::Request' ? _build_env_from_request(@_) : _build_request_from_env(@_); for my $app (@{$_dispatcher->apps}) { for my $route (@{$app->routes->{lc($request->method)}}) { if ( $route->match($request) ) { return 1; } } } return 0; } sub route_exists { carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->ok( _find_route_match($_[0]), $_[1]); } sub route_doesnt_exist { carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->ok( !_find_route_match($_[0]), $_[1]); } sub response_status_isnt { my ( $req, $status, $test_name ) = @_; carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; $test_name ||= "response status is not $status for " . _req_label($req); my $response = dancer_response($req); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->isnt_eq( $response->[0], $status, $test_name ); } { # Map comparison operator names to human-friendly ones my %cmp_name = ( is_eq => "is", isnt_eq => "is not", like => "matches", unlike => "doesn't match", ); sub _cmp_response_content { my ( $req, $want, $test_name, $cmp ) = @_; if ( @_ == 3 ) { $cmp = $test_name; $test_name = $cmp_name{$cmp}; $test_name = "response content $test_name $want for " . _req_label($req); } my $response = dancer_response($req); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $tb->$cmp( $response->[2][0], $want, $test_name ); } } sub response_content_is { carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; local $Test::Builder::Level = $Test::Builder::Level + 1; _cmp_response_content( @_, 'is_eq' ); } sub response_content_isnt { carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; local $Test::Builder::Level = $Test::Builder::Level + 1; _cmp_response_content( @_, 'isnt_eq' ); } sub response_content_like { carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; local $Test::Builder::Level = $Test::Builder::Level + 1; _cmp_response_content( @_, 'like' ); } sub response_content_unlike { carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; local $Test::Builder::Level = $Test::Builder::Level + 1; _cmp_response_content( @_, 'unlike' ); } sub response_content_is_deeply { my ( $req, $matcher, $test_name ) = @_; carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; $test_name ||= "response content looks good for " . _req_label($req); local $Test::Builder::Level = $Test::Builder::Level + 1; my $response = _req_to_response($req); is_deeply $response->[2][0], $matcher, $test_name; } sub response_is_file { my ( $req, $test_name ) = @_; carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; $test_name ||= "a file is returned for " . _req_label($req); my $response = _get_file_response($req); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; return $tb->ok( defined($response), $test_name ); } sub response_headers_are_deeply { my ( $req, $expected, $test_name ) = @_; carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; $test_name ||= "headers are as expected for " . _req_label($req); local $Test::Builder::Level = $Test::Builder::Level + 1; my $response = dancer_response( _expand_req($req) ); is_deeply( _sort_headers( $response->[1] ), _sort_headers($expected), $test_name ); } sub response_headers_include { my ( $req, $expected, $test_name ) = @_; carp 'Dancer2::Test is deprecated, please use Plack::Test instead' unless $NO_WARN; $test_name ||= "headers include expected data for " . _req_label($req); my $tb = Test::Builder->new; my $response = dancer_response($req); local $Test::Builder::Level = $Test::Builder::Level + 1; print STDERR "Headers are: " . Dumper( $response->[1] ) . "\n Expected to find header: " . Dumper($expected) if !$tb->ok( _include_in_headers( $response->[1], $expected ), $test_name ); } sub route_pod_coverage { require Pod::Simple::Search; require Pod::Simple::SimpleTree; my $all_routes = {}; foreach my $app ( @{ $_dispatcher->apps } ) { my $routes = $app->routes; my $available_routes = []; foreach my $method ( sort { $b cmp $a } keys %$routes ) { foreach my $r ( @{ $routes->{$method} } ) { # we don't need pod coverage for head next if $method eq 'head'; push @$available_routes, $method . ' ' . $r->spec_route; } } ## copy dereferenced array $all_routes->{ $app->name }{routes} = [@$available_routes] if @$available_routes; # Pod::Simple v3.30 excluded the current directory even when in @INC. # include the current directory as a search path; its backwards compatible # with previous version. my $undocumented_routes = []; my $file = Pod::Simple::Search->new->find( $app->name, '.' ); if ($file) { $all_routes->{ $app->name }{has_pod} = 1; my $parser = Pod::Simple::SimpleTree->new->parse_file($file); my $pod_dataref = $parser->root; my $found_routes = {}; for ( my $i = 0; $i < @$available_routes; $i++ ) { my $r = $available_routes->[$i]; my $app_string = lc $r; $app_string =~ s/\*/_REPLACED_STAR_/g; for ( my $idx = 0; $idx < @$pod_dataref; $idx++ ) { my $pod_part = $pod_dataref->[$idx]; next if !is_arrayref($pod_part); foreach my $ref_part (@$pod_part) { is_arrayref($ref_part) and push @$pod_dataref, $ref_part; } my $pod_string = lc $pod_part->[2]; $pod_string =~ s/['|"|\s]+/ /g; $pod_string =~ s/\s$//g; $pod_string =~ s/\*/_REPLACED_STAR_/g; if ( $pod_string =~ m/^$app_string$/ ) { $found_routes->{$app_string} = 1; next; } } if ( !$found_routes->{$app_string} ) { push @$undocumented_routes, $r; } } } else { ### no POD found $all_routes->{ $app->name }{has_pod} = 0; } if (@$undocumented_routes) { $all_routes->{ $app->name }{undocumented_routes} = $undocumented_routes; } elsif ( !$all_routes->{ $app->name }{has_pod} && @{ $all_routes->{ $app->name }{routes} } ) { ## copy dereferenced array $all_routes->{ $app->name }{undocumented_routes} = [ @{ $all_routes->{ $app->name }{routes} } ]; } } return $all_routes; } sub is_pod_covered { my ($test_name) = @_; $test_name ||= "is pod covered"; my $route_pod_coverage = route_pod_coverage(); my $tb = Test::Builder->new; local $Test::Builder::Level = $Test::Builder::Level + 1; foreach my $app ( @{ $_dispatcher->apps } ) { my %undocumented_route = ( map { $_ => 1 } @{ $route_pod_coverage->{ $app->name }{undocumented_routes} } ); $tb->subtest( $app->name . $test_name, sub { foreach my $route ( @{ $route_pod_coverage->{ $app->name }{routes} } ) { ok( !$undocumented_route{$route}, "$route is documented" ); } } ); } } sub import { my ( $class, %options ) = @_; my @applications; if ( ref $options{apps} eq ref( [] ) ) { @applications = @{ $options{apps} }; } else { my ( $caller, $script ) = caller; # if no app is passed, assume the caller is one. @applications = ($caller) if $caller->can('dancer_app'); } # register the apps to the test dispatcher $_dispatcher->apps( [ map { $_->dancer_app->finish(); $_->dancer_app; } @applications ] ); $class->export_to_level( 1, $class, @EXPORT ); } # private sub _req_label { my $req = shift; return ref $req eq 'Dancer2::Core::Response' ? 'response object' : ref $req eq 'Dancer2::Core::Request' ? join( ' ', map { $req->$_ } qw/ method path / ) : is_arrayref($req) ? join( ' ', @$req ) : "GET $req"; } sub _expand_req { my $req = shift; return is_arrayref($req) ? @$req : ( 'GET', $req ); } # Sort arrayref of headers (turn it into a list of arrayrefs, sort by the header # & value, then turn it back into an arrayref) sub _sort_headers { my @originalheaders = @{ shift() }; # take a copy we can modify my @headerpairs; while ( my ( $header, $value ) = splice @originalheaders, 0, 2 ) { push @headerpairs, [ $header, $value ]; } # We have an array of arrayrefs holding header => value pairs; sort them by # header then value, and return them flattened back into an arrayref return [ map {@$_} sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @headerpairs ]; } # make sure the given header sublist is included in the full headers array sub _include_in_headers { my ( $full_headers, $expected_subset ) = @_; # walk through all the expected header pairs, make sure # they exist with the same value in the full_headers list # return false as soon as one is not. for ( my $i = 0; $i < scalar(@$expected_subset); $i += 2 ) { my ( $name, $value ) = ( $expected_subset->[$i], $expected_subset->[ $i + 1 ] ); return 0 unless _check_header( $full_headers, $name, $value ); } # we've found all the expected pairs in the $full_headers list return 1; } sub _check_header { my ( $headers, $key, $value ) = @_; for ( my $i = 0; $i < scalar(@$headers); $i += 2 ) { my ( $name, $val ) = ( $headers->[$i], $headers->[ $i + 1 ] ); return 1 if $name eq $key && $value eq $val; } return 0; } sub _req_to_response { my $req = shift; # already a response object return $req if ref $req eq 'Dancer2::Core::Response'; return dancer_response( is_arrayref($req) ? @$req : ( 'GET', $req ) ); } # make sure we have at least one app in the dispatcher, and if not, # we must have at this point an app within the caller sub _find_dancer_apps_for_dispatcher { return if scalar( @{ $_dispatcher->apps } ); for ( my $deep = 0; $deep < 5; $deep++ ) { my $caller = caller($deep); next if !$caller || !$caller->can('dancer_app'); return $_dispatcher->apps( [ $caller->dancer_app ] ); } croak "Unable to find a Dancer2 app, did you use Dancer2 in your test?"; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Test - Useful routines for testing Dancer2 apps =head1 VERSION version 0.205002 =head1 SYNOPSIS use Test::More; use Plack::Test; use HTTP::Request::Common; # install separately use YourDancerApp; my $app = YourDancerApp->to_app; my $test = Plack::Test->create($app); my $res = $test->request( GET '/' ); is( $res->code, 200, '[GET /] Request successful' ); like( $res->content, qr/hello, world/, '[GET /] Correct content'; done_testing; =head1 DESCRIPTION B. Please use L instead as shown in the SYNOPSIS! This module will warn for a while until we actually remove it. This is to provide enough time to fully remove it from your system. If you need to remove the warnings, for now, you can set: $Dancer::Test::NO_WARN = 1; This module provides useful routines to test Dancer2 apps. They are, however, buggy and unnecessary. L is advised instead. $test_name is always optional. =head1 FUNCTIONS =head2 dancer_response ($method, $path, $params, $arg_env); Returns a Dancer2::Core::Response object for the given request. Only $method and $path are required. $params is a hashref with 'body' as a string; 'headers' can be an arrayref or a HTTP::Headers object, 'files' can be arrayref of hashref, containing some files to upload: dancer_response($method, $path, { params => $params, body => $body, headers => $headers, files => [ { filename => '/path/to/file', name => 'my_file' } ], } ); A good reason to use this function is for testing POST requests. Since POST requests may not be idempotent, it is necessary to capture the content and status in one shot. Calling the response_status_is and response_content_is functions in succession would make two requests, each of which could alter the state of the application and cause Schrodinger's cat to die. my $response = dancer_response POST => '/widgets'; is $response->status, 202, "response for POST /widgets is 202"; is $response->content, "Widget #1 has been scheduled for creation", "response content looks good for first POST /widgets"; $response = dancer_response POST => '/widgets'; is $response->status, 202, "response for POST /widgets is 202"; is $response->content, "Widget #2 has been scheduled for creation", "response content looks good for second POST /widgets"; It's possible to test file uploads: post '/upload' => sub { return upload('image')->content }; $response = dancer_response(POST => '/upload', {files => [{name => 'image', filename => '/path/to/image.jpg'}]}); In addition, you can supply the file contents as the C key: my $data = 'A test string that will pretend to be file contents.'; $response = dancer_response(POST => '/upload', { files => [{name => 'test', filename => "filename.ext", data => $data}] }); You can also supply a hashref of headers: headers => { 'Content-Type' => 'text/plain' } =head2 response_status_is ($request, $expected, $test_name); Asserts that Dancer2's response for the given request has a status equal to the one given. response_status_is [GET => '/'], 200, "response for GET / is 200"; =head2 route_exists([$method, $path], $test_name) Asserts that the given request matches a route handler in Dancer2's registry. If the route would have returned a 404, the route still exists and this test will pass. Note that because Dancer2 uses the default route handler L to match files in the public folder when no other route matches, this test will always pass. You can disable the default route handlers in the configs but you probably want L or L route_exists [GET => '/'], "GET / is handled"; =head2 route_doesnt_exist([$method, $path], $test_name) Asserts that the given request does not match any route handler in Dancer2's registry. Note that this test is likely to always fail as any route not matched will be handled by the default route handler in L. This can be disabled in the configs. route_doesnt_exist [GET => '/bogus_path'], "GET /bogus_path is not handled"; =head2 response_status_isnt([$method, $path], $status, $test_name) Asserts that the status of Dancer2's response is not equal to the one given. response_status_isnt [GET => '/'], 404, "response for GET / is not a 404"; =head2 response_content_is([$method, $path], $expected, $test_name) Asserts that the response content is equal to the C<$expected> string. response_content_is [GET => '/'], "Hello, World", "got expected response content for GET /"; =head2 response_content_isnt([$method, $path], $not_expected, $test_name) Asserts that the response content is not equal to the C<$not_expected> string. response_content_isnt [GET => '/'], "Hello, World", "got expected response content for GET /"; =head2 response_content_like([$method, $path], $regexp, $test_name) Asserts that the response content for the given request matches the regexp given. response_content_like [GET => '/'], qr/Hello, World/, "response content looks good for GET /"; =head2 response_content_unlike([$method, $path], $regexp, $test_name) Asserts that the response content for the given request does not match the regexp given. response_content_unlike [GET => '/'], qr/Page not found/, "response content looks good for GET /"; =head2 response_content_is_deeply([$method, $path], $expected_struct, $test_name) Similar to response_content_is(), except that if response content and $expected_struct are references, it does a deep comparison walking each data structure to see if they are equivalent. If the two structures are different, it will display the place where they start differing. response_content_is_deeply [GET => '/complex_struct'], { foo => 42, bar => 24}, "got expected response structure for GET /complex_struct"; =head2 response_is_file ($request, $test_name); =head2 response_headers_are_deeply([$method, $path], $expected, $test_name) Asserts that the response headers data structure equals the one given. response_headers_are_deeply [GET => '/'], [ 'X-Powered-By' => 'Dancer2 1.150' ]; =head2 response_headers_include([$method, $path], $expected, $test_name) Asserts that the response headers data structure includes some of the defined ones. response_headers_include [GET => '/'], [ 'Content-Type' => 'text/plain' ]; =head2 route_pod_coverage() Returns a structure describing pod coverage in your apps for one app like this: package t::lib::TestPod; use Dancer2; =head1 NAME TestPod =head2 ROUTES =over =cut =item get "/in_testpod" testpod =cut get '/in_testpod' => sub { return 'get in_testpod'; }; get '/hello' => sub { return "hello world"; }; =item post '/in_testpod/*' post in_testpod =cut post '/in_testpod/*' => sub { return 'post in_testpod'; }; =back =head2 SPECIALS =head3 PUBLIC =over =item get "/me:id" =cut get "/me:id" => sub { return "ME"; }; =back =head3 PRIVAT =over =item post "/me:id" post /me:id =cut post "/me:id" => sub { return "ME"; }; =back =cut 1; route_pod_coverage; would return something like: { 't::lib::TestPod' => { 'has_pod' => 1, 'routes' => [ "post /in_testpod/*", "post /me:id", "get /in_testpod", "get /hello", "get /me:id" ], 'undocumented_routes' => [ "get /hello" ] } } =head2 is_pod_covered('is pod covered') Asserts that your apps have pods for all routes is_pod_covered 'is pod covered' to avoid test failures, you should document all your routes with one of the following: head1, head2,head3,head4, item. ex: =item get '/login' route to login =cut if you use: any '/myaction' => sub { # code } or any ['get', 'post'] => '/myaction' => sub { # code }; you need to create pods for each one of the routes created there. =head2 import When Dancer2::Test is imported, it should be passed all the applications that are supposed to be tested. If none passed, then the caller is supposed to be the sole application to test. # t/sometest.t use t::lib::Foo; use t::lib::Bar; use Dancer2::Test apps => ['t::lib::Foo', 't::lib::Bar']; =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2017 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut config_multiapp.t100644001751001751 113013171470520 16515 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use File::Spec; use lib '.'; use t::app::t1::lib::App1; use t::app::t1::lib::Sub::App2; use t::app::t2::lib::App3; for my $app ( @{ Dancer2->runner->apps } ) { # Need to determine path to config; use apps' name for now.. my $path = $app->name eq 'App3' ? 't2' : 't1'; is_deeply $app->config_files, [ File::Spec->rel2abs(File::Spec->catfile( 't', 'app', $path, 'config.yml' )) ], $app->name . ": config files found"; is $app->config->{app}->{config}, 'ok', $app->name . ": config loaded properly" } done_testing; template_simple.t100644001751001751 526513171470520 16536 0ustar00jasonjason000000000000Dancer2-0.205002/tuse Test::More tests => 9; use strict; use warnings; use Dancer2::FileUtils 'path'; use Dancer2::Template::Simple; { package Foo; use Moo; has x => ( is => 'rw'); has y => ( is => 'rw'); sub method { "yeah" } } # variable interpolation, with file-based template my $engine = Dancer2::Template::Simple->new; my $template = path('t', 'views', 'template_simple_index.tt'); my $result = $engine->render( $template, { var1 => "xxx", var2 => "yyy", foo => 'one', bar => 'two', baz => 'three'}); my $expected = 'this is var1="xxx" and var2=yyy'."\n\nanother line\n\n one two three\n\nxxx/xxx\n"; is $result, $expected, "template got processed successfully"; # variable interpolation, with scalar-based template $expected = "one=1, two=2, three=3 - 77"; $template = "one=<% one %>, two=<% two %>, three=<% three %> - <% hash.key %>"; eval { $engine->render($template, { one => 1, two => 2, three => 3}) }; like $@, qr/Can't open .* using mode '<'/, "prototype failure detected"; $result = $engine->render(\$template, { one => 1, two => 2, three => 3, hash => { key => 77 }, }); is $result, $expected, "processed a template given as a scalar ref"; # complex variable interpolation (object, coderef and hash) my $foo = Foo->new; $foo->x(42); $template = 'foo->x == <% foo.x %> foo.method == <% foo.method %> foo.dumb=\'<% foo.dumb %>\''; $expected = 'foo->x == 42 foo.method == yeah foo.dumb=\'\''; $result = $engine->render(\$template, { foo => $foo }); is $result, $expected, 'object are interpolated in templates'; $template = 'code = <% code %>, code <% hash.code %>'; $expected = 'code = 42, code 42'; $result = $engine->render(\$template, { code => sub { 42 }, hash => { code => sub { 42 } } }); is $result, $expected, 'code ref are interpolated in templates'; $template = 'array: <% array %>, hash.array: <% hash.array %>'; $expected = 'array: 1 2 3 4 5, hash.array: 6 7 8'; $result = $engine->render(\$template, { array => [1, 2, 3, 4, 5], hash => { array => [6, 7, 8] }}); is $result, $expected, "arrayref are interpolated in templates"; # if-then-else $template = '<% if want %>hello<% else %>goodbye<% end %> world'; $result = $engine->render(\$template, {want => 1}); is $result, 'hello world', "true boolean condition matched"; $result = $engine->render(\$template, {want => 0}); is $result, 'goodbye world', "false boolean condition matched"; $template = 'one: 1 two: <% two %> three : <% three %>'; $result = $engine->render(\$template, {two => 2, three => 3 }); is $result, 'one: 1 two: 2 three : 3', "multiline template processed"; serializer_json.t100644001751001751 317213171470520 16547 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use JSON::MaybeXS; use Dancer2::Serializer::JSON; # config { package MyApp; use Dancer2; our $entity; set engines => { serializer => { JSON => { pretty => 1, } } }; set serializer => 'JSON'; get '/serialize' => sub { return $entity; }; } my @tests = ( { entity => { a => 1, b => 2, }, options => { pretty => 1 }, name => "basic hash", }, { entity => { c => [ { d => 3, e => { f => 4, g => 'word', } } ], h => 6 }, options => { pretty => 1 }, name => "nested", }, { entity => { data => "\x{2620}" x 10 }, options => { pretty => 1, utf8 => 1 }, name => "utf8", } ); my $app = MyApp->to_app; for my $test (@tests) { my $expected = JSON::MaybeXS->new($test->{options})->encode($test->{entity}); # Helpers pass options my $actual = Dancer2::Serializer::JSON::to_json( $test->{entity}, $test->{options} ); is( $actual, $expected, "to_json: $test->{name}" ); # Options from config my $serializer = Dancer2::Serializer::JSON->new(config => $test->{options}); my $output = $serializer->serialize( $test->{entity} ); is( $output, $expected, "serialize: $test->{name}" ); $MyApp::entity = $test->{entity}; test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/serialize' ); is($res->content, $expected, "serialized content in response: $test->{name}"); }; } done_testing(); config_settings.t100644001751001751 132213171470520 16525 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Dancer2; # testing default values is( setting('port'), '3000', "default value for 'port' is OK" ); is( setting('content_type'), 'text/html', "default value for 'content_type' is OK" ); #should we test for all default values? # testing new settings ok( setting( 'foo' => '42' ), 'setting a new value' ); is( setting('foo'), 42, 'new value has been set' ); # test the alias 'set' ok( set( bar => 43 ), 'setting bar with set' ); is( setting('bar'), 43, 'new value has been set' ); #multiple values ok( setting( 'foo' => 43, bar => 44 ), 'set multiple values' ); ok( setting('foo') == 43 && setting('bar') == 44, 'set multiple values successful' ); done_testing; session_engines.t100644001751001751 620413171470520 16537 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use YAML; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; use File::Spec; use File::Basename 'dirname'; my $SESSION_DIR; BEGIN { $SESSION_DIR = File::Spec->catfile( dirname(__FILE__), 'sessions' ); } { package App; use Dancer2; my @to_destroy; set engines => { session => { YAML => { session_dir => $SESSION_DIR } } }; hook 'engine.session.before_destroy' => sub { my $session = shift; push @to_destroy, $session; }; get '/set_session/*' => sub { my ($name) = splat; session name => $name; }; get '/read_session' => sub { my $name = session('name') || ''; "name='$name'"; }; get '/clear_session' => sub { session name => undef; return exists( session->data->{'name'} ) ? "failed" : "cleared"; }; get '/cleanup' => sub { app->destroy_session; return scalar(@to_destroy); }; setting session => 'Simple'; set( show_errors => 1, environment => 'production', ); } my $url = "http://localhost"; my $test = Plack::Test->create( App->to_app ); my $app = Dancer2->runner->apps->[0]; my @clients = qw(one two three); for my $engine ( qw(YAML Simple) ) { # clear current session engine, and rebuild for the test # This is *really* messy, playing in object hashrefs.. delete $app->{session_engine}; $app->config->{session} = $engine; $app->session_engine; # trigger a build # run the tests for this engine for my $client (@clients) { my $jar = HTTP::Cookies->new; subtest "[$engine][$client] Empty session" => sub { my $res = $test->request( GET "$url/read_session" ); like $res->content, qr/name=''/, "empty session for client $client"; $jar->extract_cookies($res); }; subtest "[$engine][$client] set_session" => sub { my $req = GET "$url/set_session/$client"; $jar->add_cookie_header($req); my $res = $test->request($req); ok( $res->is_success, "set_session for client $client" ); $jar->extract_cookies($res); }; subtest "[$engine][$client] session for client" => sub { my $req = GET "$url/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); like $res->content, qr/name='$client'/, "session looks good for client $client"; $jar->extract_cookies($res); }; subtest "[$engine][$client] delete session" => sub { my $req = GET "$url/clear_session"; $jar->add_cookie_header($req); my $res = $test->request($req); like $res->content, qr/cleared/, "deleted session key"; }; subtest "[$engine][$client] cleanup" => sub { my $req = GET "$url/cleanup"; $jar->add_cookie_header($req); my $res = $test->request($req); ok( $res->is_success, "cleanup done for $client" ); ok( $res->content, "session hook triggered" ); }; } } done_testing; plugin_register.t100644001751001751 231013171470520 16540 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More import => ['!pass']; use Test::Fatal; subtest 'reserved keywords' => sub { use Dancer2::Plugin; like( exception { register dance => sub {1} }, qr/You can't use 'dance', this is a reserved keyword/, "Can't use Dancer2's reserved keywords", ); like( exception { register '1function' => sub {1} }, qr/You can't use '1function', it is an invalid name/, "Can't use invalid names for keywords", ); }; subtest 'plugin reserved keywords' => sub { { package Foo; use Dancer2::Plugin; Test::More::is( Test::Fatal::exception { register 'foo_method' => sub {1} }, undef, "can register a new keyword", ); } { package Bar; use Dancer2::Plugin; Test::More::like( Test::Fatal::exception { register 'foo_method' => sub {1} }, qr{can't use foo_method, this is a keyword reserved by Foo}, "can't register a keyword already registered by another plugin", ); } }; done_testing; session_forward.t100644001751001751 1157213171470520 16577 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; { package Test::Forward::Single; use Dancer2; set session => 'Simple'; get '/main' => sub { session foo => 'Single/main'; forward '/outer'; }; get '/outer' => sub { session bar => 'Single/outer'; forward '/inner'; }; get '/inner' => sub { session baz => 'Single/inner'; return join ':', map +( session($_) || '' ), qw; }; get '/clear' => sub { session foo => undef; session bar => undef; session baz => undef; }; } { package Test::Forward::Multi::SameCookieName; use Dancer2; set session => 'Simple'; prefix '/same'; get '/main' => sub { session foo => 'SameCookieName/main'; forward '/outer'; }; get '/bad_chain' => sub { session foo => 'SameCookieName/bad_chain'; forward '/other/main'; }; } { package Test::Forward::Multi::OtherCookieName; use Dancer2; set engines => { session => { Simple => { cookie_name => 'session.dancer' } } }; set session => 'Simple'; prefix '/other'; get '/main' => sub { session foo => 'OtherCookieName/main'; # Forwards to another app with different cookie name forward '/outer'; }; get '/clear' => sub { session foo => undef; session bar => undef; session baz => undef; }; } # base uri for all requests. my $base = 'http://localhost'; subtest 'Forwards within a single app' => sub { my $test = Plack::Test->create( Test::Forward::Single->to_app ); my $jar = HTTP::Cookies->new; { my $res = $test->request( GET "$base/main" ); is( $res->content, q{Single/main:Single/outer:Single/inner}, 'session value preserved after chained forwards', ); $jar->extract_cookies($res); } { my $req = GET "$base/inner"; $jar->add_cookie_header($req); my $res = $test->request($req); is( $res->content, q{Single/main:Single/outer:Single/inner}, 'session values preserved between calls', ); $jar->extract_cookies($res); } { my $req = GET "$base/clear"; $jar->add_cookie_header($req); my $res = $test->request( GET "$base/clear" ); $jar->extract_cookies($res); } { my $req = GET "$base/outer"; $jar->add_cookie_header($req); my $res = $test->request( GET "$base/outer" ); is( $res->content, q{:Single/outer:Single/inner}, 'session value preserved after forward from route', ); $jar->extract_cookies($res); } }; subtest 'Forwards between multiple apps using the same cookie name' => sub { my $test = Plack::Test->create( Dancer2->psgi_app ); my $jar = HTTP::Cookies->new; { my $res = $test->request( GET "$base/same/main" ); is( $res->content, q{SameCookieName/main:Single/outer:Single/inner}, 'session value preserved after chained forwards between apps', ); $jar->extract_cookies($res); } { my $req = GET "$base/outer"; $jar->add_cookie_header($req); my $res = $test->request($req); is( $res->content, q{SameCookieName/main:Single/outer:Single/inner}, 'session value preserved after forward from route', ); } }; subtest 'Forwards between multiple apps using different cookie names' => sub { my $test = Plack::Test->create( Dancer2->psgi_app ); my $jar = HTTP::Cookies->new; my $res = $test->request( GET "$base/other/main" ); is( $res->content, q{:Single/outer:Single/inner}, 'session value only from forwarded app', ); }; # we need to make sure B doesn't override A when forwarding to C # A -> B -> C # This means that A (cookie_name "Homer") # forwarding to B (cookie_name "Marge") # forwarding to C (cookie_name again "Homer") # will cause a problem because we will lose "Homer" session data, # because it will be overwritten by "Marge" session data. # Suddenly A and C cannot communicate because it was flogged. # # if A -> Single, B -> OtherCookieName, C -> SameCookieName # call A, create session, then forward to B, create session, # then forward to C, check has values as in A and C subtest 'Forwards between multiple apps using multiple different cookie names' => sub { my $test = Plack::Test->create( Dancer2->psgi_app ); my $jar = HTTP::Cookies->new; my $res = $test->request( GET "$base/same/bad_chain" ); is( $res->content, q{SameCookieName/bad_chain:Single/outer:Single/inner}, 'session value only from apps with same session cookie name', ); }; done_testing; config000755001751001751 013171470520 14262 5ustar00jasonjason000000000000Dancer2-0.205002/tconfig.yml100644001751001751 11313171470520 16365 0ustar00jasonjason000000000000Dancer2-0.205002/t/configmain: 1 charset: 'UTF-8' show_errors: 1 application: some_feature: foo t1000755001751001751 013171470520 14121 5ustar00jasonjason000000000000Dancer2-0.205002/t/appconfig.yml100644001751001751 2413171470520 16205 0ustar00jasonjason000000000000Dancer2-0.205002/t/app/t1app: config: ok config.yml100644001751001751 2413171470520 16206 0ustar00jasonjason000000000000Dancer2-0.205002/t/app/t2app: config: ok config.yml100644001751001751 1713171470520 16416 0ustar00jasonjason000000000000Dancer2-0.205002/t/issueslogger: "Note" basic-2.t100644001751001751 133113171470520 16140 0ustar00jasonjason000000000000Dancer2-0.205002/t/plugin2use strict; use warnings; use Test::More tests => 8; use Plack::Test; use HTTP::Request::Common; use lib 't/lib'; use poc2; my $test = Plack::Test->create( poc2->to_app ); note "poc2 root"; { my $res = $test->request( GET '/' ); ok $res->is_success; my $content = $res->content; like $content, qr/please/; like $content, qr/8-D/; } note "pos2 goodbye"; { my $res = $test->request( GET '/goodbye' ); ok $res->is_success; my $content = $res->content; like $content, qr/farewell/; like $content, qr/please/; } note "pos2 hooked"; { my $res = $test->request( GET '/sudo' ); ok ! $res->is_success; my $content = $res->content; like $content, qr/Not in sudoers file/; } cpanfile100644001751001751 47413171470520 16443 0ustar00jasonjason000000000000Dancer2-0.205002/share/skelrequires "Dancer2" => "[d2% dancer_version %2d]"; recommends "YAML" => "0"; recommends "URL::Encode::XS" => "0"; recommends "CGI::Deurl::XS" => "0"; recommends "HTTP::Parser::XS" => "0"; on "test" => sub { requires "Test::More" => "0"; requires "HTTP::Request::Common" => "0"; }; forward_test_tcp.t100644001751001751 403113171470520 16711 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package App; use Dancer2; get '/' => sub { 'home:' . join( ',', params ); }; get '/bounce/' => sub { forward '/' }; get '/bounce/:withparams/' => sub { forward '/' }; get '/bounce2/adding_params/' => sub { forward '/', { withparams => 'foo' }; }; get '/go_to_post/' => sub { forward '/simple_post_route/', { foo => 'bar' }, { method => 'post' }; }; post '/simple_post_route/' => sub { 'post:' . join( ',', params ); }; post '/' => sub {'post-home'}; post '/bounce/' => sub { forward '/' }; } my $app = Dancer2->psgi_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; my $res = $cb->(GET "/"); is $res->code => 200; like $res->content => qr/home:/; $res = $cb->(GET "/bounce/"); is $res->code => 200; like $res->content => qr/home:/; $res = $cb->(GET "/bounce/thesethings/"); is $res->code => 200; is $res->content => 'home:withparams,thesethings'; $res = $cb->(GET "/bounce2/adding_params/"); is $res->code => 200; is $res->content => 'home:withparams,foo'; $res = $cb->(GET "/go_to_post/"); is $res->code => 200; is $res->content => 'post:foo,bar'; $res = $cb->(GET "/bounce/"); is $res->header('Content-Length') => 5; is $res->header('Content-Type') => 'text/html; charset=UTF-8'; is $res->header('Server') => "Perl Dancer2 " . Dancer2->VERSION; $res = $cb->(POST "/"); is $res->code => 200; is $res->content => 'post-home'; $res = $cb->(POST "/bounce/"); is $res->code => 200; is $res->content => 'post-home'; is $res->header('Content-Length') => 9; is $res->header('Content-Type') => 'text/html; charset=UTF-8'; is $res->header('Server') => "Perl Dancer2 " . Dancer2->VERSION; }; done_testing(); MyDancerDSL.pm100644001751001751 176113171470520 16333 0ustar00jasonjason000000000000Dancer2-0.205002/t/libpackage MyDancerDSL; use Moo; use Dancer2::Core::Hook; use Dancer2::Core::Error; use Dancer2::FileUtils; use Carp; extends 'Dancer2::Core::DSL'; around dsl_keywords => sub { my $orig = shift; my $keywords = $orig->(@_); $keywords->{gateau} = { is_global => 0 }; # cookie $keywords->{moteur} = { is_global => 1 }; # engine $keywords->{stop} = { is_global => 0 }; # halt $keywords->{prend} = { is_global => 1, prototype => '@' }; # get $keywords->{envoie} = { is_global => 1, prototype => '$&' }; # post $keywords->{entete} = { is_global => 0 }; #header $keywords->{proto} = { is_global => 1, prototype => '&' }; # prototype return $keywords; }; sub gateau { goto &Dancer2::Core::DSL::cookie } sub moteur { goto &Dancer2::Core::DSL::engine } sub stop { goto &Dancer2::Core::DSL::halt } sub prend { goto &Dancer2::Core::DSL::get } sub envoie { goto &Dancer2::Core::DSL::post } sub entete { goto &Dancer2::Core::DSL::header } sub proto { $_[1]->() } 1; auto_page.tt100644001751001751 4113171470520 16562 0ustar00jasonjason000000000000Dancer2-0.205002/t/viewsHey! This is Auto Page wörking. lib000755001751001751 013171470520 14667 5ustar00jasonjason000000000000Dancer2-0.205002/t/app/t1App1.pm100644001751001751 7113171470520 16124 0ustar00jasonjason000000000000Dancer2-0.205002/t/app/t1/libpackage App1; use strict; use warnings; use Dancer2; 1; lib000755001751001751 013171470520 14670 5ustar00jasonjason000000000000Dancer2-0.205002/t/app/t2App3.pm100644001751001751 7013171470520 16126 0ustar00jasonjason000000000000Dancer2-0.205002/t/app/t2/libpackage App3; use strict; use warnings; use Dancer2; 1; config2000755001751001751 013171470520 14344 5ustar00jasonjason000000000000Dancer2-0.205002/tconfig.yml100644001751001751 17713171470520 16461 0ustar00jasonjason000000000000Dancer2-0.205002/t/config2main: 1 charset: 'UTF-8' show_errors: 1 application: feature_1: foo feature_2: bar feature_3: baz feature_4: blat release-distmeta.t100644001751001751 40113171470520 16545 0ustar00jasonjason000000000000Dancer2-0.205002/t#!perl BEGIN { unless ($ENV{RELEASE_TESTING}) { print qq{1..0 # SKIP these tests are for release candidate testing\n}; exit } } # This file was automatically generated by Dist::Zilla::Plugin::MetaTests. use Test::CPAN::Meta; meta_yaml_ok(); Plugin.pm100644001751001751 7405613171470520 16566 0ustar00jasonjason000000000000Dancer2-0.205002/lib/Dancer2package Dancer2::Plugin; # ABSTRACT: base class for Dancer2 plugins $Dancer2::Plugin::VERSION = '0.205002'; use strict; use warnings; use Moo; use Carp; use List::Util qw/ reduce /; use Module::Runtime 'require_module'; use Attribute::Handlers; use Scalar::Util; use Ref::Util qw; our $CUR_PLUGIN; extends 'Exporter::Tiny'; with 'Dancer2::Core::Role::Hookable'; has app => ( is => 'ro', weak_ref => 1, required => 1, ); has config => ( is => 'ro', lazy => 1, default => sub { my $self = shift; my $config = $self->app->config; my $package = ref $self; # TODO $package =~ s/Dancer2::Plugin:://; $config->{plugins}{$package} || {}; }, ); my $_keywords = {}; sub keywords { $_keywords } my $REF_ADDR_REGEX = qr{ [A-Za-z0-9\:\_]+ =HASH \( ([0-9a-fx]+) \) }x; my %instances; # backwards compatibility our $_keywords_by_plugin = {}; has '+hooks' => ( default => sub { my $plugin = shift; my $name = 'plugin.' . lc ref $plugin; $name =~ s/Dancer2::Plugin:://i; $name =~ s/::/_/g; +{ map { join( '.', $name, $_ ) => [] } @{ $plugin->ClassHooks } }; }, ); sub add_hooks { my $class = shift; push @{ $class->ClassHooks }, @_; } sub execute_plugin_hook { my ( $self, $name, @args ) = @_; my $plugin_class = ref $self; $self->isa('Dancer2::Plugin') or croak "Cannot call plugin hook ($name) from outside plugin"; $plugin_class =~ s/^Dancer2::Plugin:://; # short names my $full_name = 'plugin.' . lc($plugin_class) . ".$name"; $full_name =~ s/::/_/g; $self->app->execute_hook( $full_name, @args ); } sub find_plugin { my ( $self, $name ) = @_; return $self->app->find_plugin($name); } # both functions are there for D2::Core::Role::Hookable # back-compatibility. Aren't used sub supported_hooks { [] } sub hook_aliases { $_[0]->{'hook_aliases'} ||= {} } ### has() STUFF ######################################## # our wrapping around Moo::has, done to be able to intercept # both 'from_config' and 'plugin_keyword' sub _p2_has { my $class = shift; $class->_p2_has_from_config( $class->_p2_has_keyword( @_ ) ); }; sub _p2_has_from_config { my( $class, $name, %args ) = @_; my $config_name = delete $args{'from_config'} or return ( $name, %args ); $args{lazy} = 1; if ( is_coderef($config_name) ) { $args{default} ||= $config_name; $config_name = 1; } $config_name = $name if $config_name eq '1'; my $orig_default = $args{default} || sub{}; $args{default} = sub { my $plugin = shift; my $value = reduce { eval { $a->{$b} } } $plugin->config, split /\./, $config_name; return defined $value ? $value: $orig_default->($plugin); }; return $name => %args; } sub _p2_has_keyword { my( $class, $name, %args ) = @_; if( my $keyword = delete $args{plugin_keyword} ) { $keyword = $name if $keyword eq '1'; $class->keywords->{$_} = sub { (shift)->$name(@_) } for ref $keyword ? @$keyword : $keyword; } return $name => %args; } ### ATTRIBUTE HANDLER STUFF ######################################## # :PluginKeyword shenanigans sub PluginKeyword :ATTR(CODE,BEGIN) { my( $class, $sym_ref, $code, undef, $args ) = @_; # importing at BEGIN stage doesn't work with 5.10 :-( return unless ref $sym_ref; my $func_name = *{$sym_ref}{NAME}; $args = join '', @$args if is_arrayref($args); for my $name ( split ' ', $args || $func_name ) { $class->keywords->{$name} = $code; } } ## EXPORT STUFF ############################################################## # this @EXPORT will only be taken # into account when we do a 'use Dancer2::Plugin' # I.e., it'll only do its magic for the # plugins themselves, not when they are # called our @EXPORT = qw/ :plugin /; # compatibility - it will be removed soon! my $no_dsl = {}; my $exported_app = {}; sub _exporter_expand_tag { my( $class, $name, $args, $global ) = @_; my $caller = $global->{into}; $name eq 'no_dsl' and $no_dsl->{$caller} = 1; # no_dsl check here is for compatibility only # it will be removed soon! return _exporter_plugin($caller) if $name eq 'plugin' or $name eq 'no_dsl'; return _exporter_app($class,$caller,$global) if $name eq 'app' and $caller->can('app') and !$no_dsl->{$class}; return; } # plugin has been called within a D2 app. Modify # the app and export keywords sub _exporter_app { my( $class, $caller, $global ) = @_; $exported_app->{$caller} = 1; # The check for ->dsl->app is to handle plugins as well. # Otherwise you can only import from a plugin to an app, # but with this, you can import to anything # that has a DSL with an app, which translates to "also plugins" my $app = eval("${caller}::app()") || eval { $caller->dsl->app } ## no critic qw(BuiltinFunctions::ProhibitStringyEval) or return; ## no critic return unless $app->can('with_plugin'); my $plugin = $app->with_plugin( '+' . $class ); $global->{'plugin'} = $plugin; return unless $class->can('keywords'); # Add our hooks to the app, so they're recognized # this is for compatibility so you can call execute_hook() # without the fully qualified plugin name. # The reason we need to do this here instead of when adding a # hook is because we need to register in the app, and only now it # exists. # This adds a caveat that two plugins cannot register # the same hook name, but that will be deprecated anyway. {; foreach my $hook ( @{ $plugin->ClassHooks } ) { my $full_name = 'plugin.' . lc($class) . ".$hook"; $full_name =~ s/Dancer2::Plugin:://i; $full_name =~ s/::/_/g; # this adds it to the plugin $plugin->hook_aliases->{$hook} = $full_name; # this adds it to the app $plugin->app->hook_aliases->{$hook} = $full_name; # copy the hooks from the plugin to the app # this is in case they were created at import time # rather than after @{ $plugin->app->hooks }{ keys %{ $plugin->hooks } } = values %{ $plugin->hooks }; } } { # get the reference my ($plugin_addr) = "$plugin" =~ $REF_ADDR_REGEX; $instances{$plugin_addr}{'config'} = sub { $plugin->config }; $instances{$plugin_addr}{'app'} = $plugin->app; Scalar::Util::weaken( $instances{$plugin_addr}{'app'} ); ## no critic no strict 'refs'; # we used a forward declaration # so the compiled form "plugin_setting;" can be overridden # with this implementation, # which works on runtime ("plugin_setting()") # we can't use can() here because the forward declaration will # create a CODE stub no warnings 'redefine'; *{"${class}::plugin_setting"} = sub { my ($plugin_addr) = "$CUR_PLUGIN" =~ $REF_ADDR_REGEX; $plugin_addr or Carp::croak('Can\'t find originating plugin'); # we need to do this because plugins might call "set" # in order to change plugin configuration but it doesn't # change the plugin object, it changes the app object # so we merge them. my $name = ref $CUR_PLUGIN; $name =~ s/^Dancer2::Plugin:://g; my $plugin_inst = $instances{$plugin_addr}; my $plugin_config = $plugin_inst->{'config'}->(); my $app_plugin_config = $plugin_inst->{'app'}->config->{'plugins'}{$name}; return { %{ $plugin_config || {} }, %{ $app_plugin_config || {} } }; }; # FIXME: # why doesn't this work? it's like it's already defined somewhere # but i'm not sure where. seems like AUTOLOAD runs it. #$class->can('execute_hook') or *{"${class}::execute_hook"} = sub { # this can also be called by App.pm itself # if the plugin is a # "candidate" for a hook # See: App.pm "execute_hook" method, "around" modifier if ( $_[0]->isa('Dancer2::Plugin') ) { # this means it's probably our hook, we need to verify it my ( $plugin_self, $hook_name, @args ) = @_; my $plugin_class = lc $class; $plugin_class =~ s/^dancer2::plugin:://; $plugin_class =~ s{::}{_}g; # you're either calling it with the full qualifier or not # if not, use the execute_plugin_hook instead if ( $plugin->hooks->{"plugin.$plugin_class.$hook_name"} ) { Carp::carp("Please use fully qualified hook name or " . "the method execute_plugin_hook"); $hook_name = "plugin.$plugin_class.$hook_name"; } $hook_name =~ /^plugin\.$plugin_class/ or Carp::croak('Unknown plugin called through other plugin'); # now we can't really use the app to execute it because # the "around" modifier is the one calling us to begin # with, so we need to call it directly ourselves # this is okay because the modifier is there only to # call candidates, like us (this is in fact how and # why we were called) $_->( $plugin_self, @args ) for @{ $plugin->hooks->{$hook_name} }; return; } return $plugin->app->execute_hook(@_); }; } local $CUR_PLUGIN = $plugin; $_->($plugin) for @{ $plugin->_DANCER2_IMPORT_TIME_SUBS() }; map { [ $_ => {plugin => $plugin} ] } keys %{ $plugin->keywords }; } # turns the caller namespace into # a D2P2 class, with exported keywords sub _exporter_plugin { my $caller = shift; require_module('Dancer2::Core::DSL'); my $keywords_list = join ' ', keys %{ Dancer2::Core::DSL->dsl_keywords }; eval <<"END"; ## no critic { package $caller; use Moo; use Carp (); use Attribute::Handlers; extends 'Dancer2::Plugin'; our \@EXPORT = ( ':app' ); around has => sub { my( \$orig, \@args ) = \@_; \$orig->( ${caller}->_p2_has( \@args) ); }; sub PluginKeyword :ATTR(CODE,BEGIN) { goto &Dancer2::Plugin::PluginKeyword; } sub execute_plugin_hook { goto &Dancer2::Plugin::execute_plugin_hook; } my \$_keywords = {}; sub keywords { \$_keywords } my \$_ClassHooks = []; sub ClassHooks { \$_ClassHooks } # this is important as it'll do the keywords mapping between the # plugin and the app sub register_plugin { Dancer2::Plugin::register_plugin(\@_) } sub register { my ( \$keyword, \$sub ) = \@_; \$_keywords->{\$keyword} = \$sub; \$keyword =~ /^[a-zA-Z_]+[a-zA-Z0-9_]*\$/ or Carp::croak( "You can't use '\$keyword', it is an invalid name" . " (it should match ^[a-zA-Z_]+[a-zA-Z0-9_]*\\\$ )"); grep +( \$keyword eq \$_ ), qw<$keywords_list> and Carp::croak("You can't use '\$keyword', this is a reserved keyword"); \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword} and Carp::croak("You can't use \$keyword, " . "this is a keyword reserved by " . \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword}); \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword} = "$caller"; # Exporter::Tiny doesn't seem to generate the subs # in the caller properly, so we have to do it manually { no strict 'refs'; *{"${caller}::\$keyword"} = \$sub; } } my \@_DANCER2_IMPORT_TIME_SUBS; sub _DANCER2_IMPORT_TIME_SUBS {\\\@_DANCER2_IMPORT_TIME_SUBS} sub on_plugin_import (&) { push \@_DANCER2_IMPORT_TIME_SUBS, \$_[0]; } sub register_hook { goto &plugin_hooks } sub plugin_setting {}; sub plugin_args { Carp::carp "Plugin DSL method 'plugin_args' is deprecated. " . "Use '\\\@_' instead'.\n"; \@_; } } END $no_dsl->{$caller} or eval <<"END"; ## no critic { package $caller; # FIXME: AUTOLOAD might pick up on this sub dancer_app { Carp::carp "Plugin DSL method 'dancer_app' is deprecated. " . "Use '\\\$self->app' instead'.\n"; \$_[0]->app; } # FIXME: AUTOLOAD might pick up on this sub request { Carp::carp "Plugin DSL method 'request' is deprecated. " . "Use '\\\$self->app->request' instead'.\n"; \$_[0]->app->request; } # FIXME: AUTOLOAD might pick up on this sub var { Carp::carp "Plugin DSL method 'var' is deprecated. " . "Use '\\\$self->app->request->var' instead'.\n"; shift->app->request->var(\@_); } # FIXME: AUTOLOAD might pick up on this sub hook { Carp::carp "Plugin DSL method 'hook' is deprecated. " . "Use '\\\$self->app->add_hook' instead'.\n"; shift->app->add_hook( Dancer2::Core::Hook->new( name => shift, code => shift ) ); } } END die $@ if $@; my $app_dsl_cb = _find_consumer(); if ( $app_dsl_cb ) { my $dsl = $app_dsl_cb->(); { ## no critic qw(TestingAndDebugging::ProhibitNoWarnings) no strict 'refs'; no warnings 'redefine'; *{"${caller}::dsl"} = sub {$dsl}; } } return map { [ $_ => { class => $caller } ] } qw/ plugin_keywords plugin_hooks /; } sub _find_consumer { my $class; ## no critic qw(ControlStructures::ProhibitCStyleForLoops) for ( my $i = 1; my $caller = caller($i); $i++ ) { $class = $caller->can('dsl') and last; } # If you use a Dancer2 plugin outside a Dancer App, this fails. # It also breaks a bunch of the tests. -- SX #$class # or croak('Could not find Dancer2 app'); return $class; }; # This has to be called for now at the end of every plugin package, in order to # map the keywords of the associated app to the plugin, so that these keywords # can be called from within the plugin code. This function is deprecated, as # it's tied to the old plugin system. It's kept here for backcompat reason, but # should go away with the old plugin system. sub register_plugin { my $plugin_module = caller(1); # if you ask yourself why we do the injection in the plugin # module namespace every time the plugin is used, and not only # once, it's because it can be used by different app that could # have a different DSL with a different list of keywords. my $_DANCER2_IMPORT_TIME_SUBS = $plugin_module->_DANCER2_IMPORT_TIME_SUBS; unshift(@$_DANCER2_IMPORT_TIME_SUBS, sub { my $app_dsl_cb = _find_consumer(); # Here we want to verify that "register_plugin" compat keyword # was in fact only called from an app. $app_dsl_cb or Carp::croak( 'I could not find a Dancer App for this plugin'); my $dsl = $app_dsl_cb->(); foreach my $keyword ( keys %{ $dsl->dsl_keywords} ) { # if not yet defined, inject the keyword in the plugin # namespace, but make sure the code will always get the # coderef from the right associated app, because one plugin # can be used by multiple apps. Note that we remove the # first parameter (plugin instance) from what we pass to # the keyword implementation of the App no strict 'refs'; $plugin_module->can($keyword) or *{"${plugin_module}::$keyword"} = sub { my $coderef = shift()->app->name->can($keyword); $coderef->(@_); }; } }); } sub _exporter_expand_sub { my( $plugin, $name, $args, $global ) = @_; my $class = $args->{class}; return _exported_plugin_keywords($plugin,$class) if $name eq 'plugin_keywords'; return _exported_plugin_hooks($class) if $name eq 'plugin_hooks'; $exported_app->{ $global->{'into'} } or Carp::croak('Specific subroutines cannot be exported from plugin'); # otherwise, we're exporting a keyword my $p = $args->{plugin}; my $sub = $p->keywords->{$name}; return $name => sub(@) { # localize the plugin so we can get it later local $CUR_PLUGIN = $p; $sub->($p,@_); } } # "There's a good reason for this, I swear!" # -- Sawyer X # basically, if someone adds a hook to the app directly # that needs to access a DSL that needs the current object # (such as "plugin_setting"), # that object needs to be available # So: # we override App's "add_hook" to provide a register a # different hook callback, that closes over the plugin when # it's available, relocalizes it when the callback runs and # after localizing it, calls the original hook callback { ## no critic; no strict 'refs'; no warnings 'redefine'; my $orig_cb = Dancer2::Core::App->can('add_hook'); $orig_cb and *{'Dancer2::Core::App::add_hook'} = sub { my ( $app, $hook ) = @_; my $hook_code = Scalar::Util::blessed($hook) ? $hook->code : $hook->{code}; my $plugin = $CUR_PLUGIN; $hook->{'code'} = sub { local $CUR_PLUGIN = $plugin; $hook_code->(@_); }; $orig_cb->(@_); }; } # define the exported 'plugin_keywords' sub _exported_plugin_keywords{ my( $plugin, $class ) = @_; return plugin_keywords => sub(@) { while( my $name = shift @_ ) { ## no critic my $sub = is_coderef($_[0]) ? shift @_ : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name ); $class->keywords->{$_} = $sub for ref $name ? @$name : $name; } } } sub _exported_plugin_hooks { my $class = shift; return plugin_hooks => sub (@) { $class->add_hooks(@_) } } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Plugin - base class for Dancer2 plugins =head1 VERSION version 0.205002 =head1 SYNOPSIS The plugin itself: package Dancer2::Plugin::Polite; use strict; use warnings; use Dancer2::Plugin; has smiley => ( is => 'ro', default => sub { $_[0]->config->{smiley} || ':-)' } ); plugin_keywords 'add_smileys'; sub BUILD { my $plugin = shift; $plugin->app->add_hook( Dancer2::Core::Hook->new( name => 'after', code => sub { $_[0]->content( $_[0]->content . " ... please?" ) } )); $plugin->app->add_route( method => 'get', regexp => '/goodbye', code => sub { my $app = shift; 'farewell, ' . $app->request->params->{name}; }, ); } sub add_smileys { my( $plugin, $text ) = @_; $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; return $text; } 1; then to load into the app: package MyApp; use strict; use warnings; use Dancer2; BEGIN { # would usually be in config.yml set plugins => { Polite => { smiley => '8-D', }, }; } use Dancer2::Plugin::Polite; get '/' => sub { add_smileys( 'make me a sandwich.' ); }; 1; =head1 DESCRIPTION =head2 Writing the plugin =head3 C The plugin must begin with use Dancer2::Plugin; which will turn the package into a L class that inherits from L. The base class provides the plugin with two attributes: C, which is populated with the Dancer2 app object for which the plugin is being initialized for, and C which holds the plugin section of the application configuration. =head3 Modifying the app at building time If the plugin needs to tinker with the application -- add routes or hooks, for example -- it can do so within its C function. sub BUILD { my $plugin = shift; $plugin->app->add_route( ... ); } =head3 Adding keywords =head4 Via C Keywords that the plugin wishes to export to the Dancer2 app can be defined via the C keyword: plugin_keywords qw/ add_smileys add_sad_kitten /; Each of the keyword will resolve to the class method of the same name. When invoked as keyword, it'll be passed the plugin object as its first argument. sub add_smileys { my( $plugin, $text ) = @_; return join ' ', $text, $plugin->smiley; } # and then in the app get '/' => sub { add_smileys( "Hi there!" ); }; You can also pass the functions directly to C. plugin_keywords add_smileys => sub { my( $plugin, $text ) = @_; $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; return $text; }, add_sad_kitten => sub { ... }; Or a mix of both styles. We're easy that way: plugin_keywords add_smileys => sub { my( $plugin, $text ) = @_; $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; return $text; }, 'add_sad_kitten'; sub add_sad_kitten { ...; } If you want several keywords to be synonyms calling the same function, you can list them in an arrayref. The first function of the list is taken to be the "real" method to link to the keywords. plugin_keywords [qw/ add_smileys add_happy_face /]; sub add_smileys { ... } Calls to C are cumulative. =head4 Via the C<:PluginKeyword> function attribute For perl 5.12 and higher, keywords can also be defined by adding the C<:PluginKeyword> attribute to the function you wish to export. For Perl 5.10, the export triggered by the sub attribute comes too late in the game, and the keywords won't be exported in the application namespace. sub foo :PluginKeyword { ... } sub bar :PluginKeyword( baz quux ) { ... } # equivalent to sub foo { ... } sub bar { ... } plugin_keywords 'foo', [ qw/ baz quux / ] => \&bar; =head4 For an attribute You can also turn an attribute of the plugin into a keyword. has foo => ( is => 'ro', plugin_keyword => 1, # keyword will be 'foo' ); has bar => ( is => 'ro', plugin_keyword => 'quux', # keyword will be 'quux' ); has baz => ( is => 'ro', plugin_keyword => [ 'baz', 'bazz' ], # keywords will be 'baz' and 'bazz' ); =head3 Accessing the plugin configuration The plugin configuration is available via the C method. sub BUILD { my $plugin = shift; if ( $plugin->config->{feeling_polite} ) { $plugin->app->add_hook( Dancer2::Core::Hook->new( name => 'after', code => sub { $_[0]->content( $_[0]->content . " ... please?" ) } )); } } =head3 Getting default values from config file Since initializing a plugin with either a default or a value passed via the configuration file, like has smiley => ( is => 'ro', default => sub { $_[0]->config->{smiley} || ':-)' } ); C allows for a C key in the attribute definition. Its value is the plugin configuration key that will be used to initialize the attribute. If it's given the value C<1>, the name of the attribute will be taken as the configuration key. Nested hash keys can also be referred to using a dot notation. If the plugin configuration has no value for the given key, the attribute default, if specified, will be honored. If the key is given a coderef as value, it's considered to be a C value combo: has foo => ( is => 'ro', from_config => sub { 'my default' }, ); # equivalent to has foo => ( is => 'ro', from_config => 'foo', default => sub { 'my default' }, ); For example: # in config.yml plugins: Polite: smiley: ':-)' greeting: casual: Hi! formal: How do you do? # in the plugin has smiley => ( # will be ':-)' is => 'ro', from_config => 1, default => sub { ':-(' }, ); has casual_greeting => ( # will be 'Hi!' is => 'ro', from_config => 'greeting.casual', ); has apology => ( # will be 'sorry' is => 'ro', from_config => 'apology', default => sub { 'sorry' }, ) has closing => ( # will be 'See ya!' is => 'ro', from_config => sub { 'See ya!' }, ); =head3 Config becomes immutable The plugin's C attribute is loaded lazily on the first call to C. After this first call C becomes immutable so you cannot do the following in a test: use Dancer2; use Dancer2::Plugin::FooBar; set plugins => { FooBar => { wibble => 1, # this is OK }, }; flibble(45); # plugin keyword called which causes config read set plugins => { FooBar => { wibble => 0, # this will NOT change plugin config }, }; =head3 Accessing the parent Dancer app If the plugin is instantiated within a Dancer app, it'll be accessible via the method C. sub BUILD { my $plugin = shift; $plugin->app->add_route( ... ); } To use Dancer's DSL in your plugin: $self->dsl->debug( “Hi! I’m logging from your plugin!” ); See L for a full list of Dancer2 DSL. =head2 Using the plugin within the app A plugin is loaded via use Dancer2::Plugin::Polite; The plugin will assume that it's loading within a Dancer module and will automatically register itself against its C and export its keywords to the local namespace. If you don't want this to happen, specify that you don't want anything imported via empty parentheses when Cing the module: use Dancer2::Plugin::Polite (); =head2 Plugins using plugins It's easy to use plugins from within a plugin: package Dancer2::Plugin::SourPuss; use Dancer2::Plugin; use Dancer2::Plugin::Polite; sub my_keyword { my $smiley = smiley(); } 1; This does not export C into your application - it is only available from within your plugin. However, from the example above, you can wrap DSL from other plugins and make it available from your plugin. =head2 Utilizing other plugins You can use the C to locate other plugins loaded by the user, in order to use them, or their information, directly: # MyApp.pm use Dancer2; use Dancer2::Plugin::Foo; use Dancer2::Plugin::Bar; # Dancer2::Plugin::Bar; ... sub my_keyword { my $self = shift; my $foo = $self->find_plugin('Dancer2::Plugin::Foo') or $self->dsl->send_error('Could not find Foo'); return $foo->foo_keyword(...); } =head2 Hooks New plugin hooks are declared via C. plugin_hooks 'my_hook', 'my_other_hook'; Hooks are prefixed with C. So the plugin C coming from the plugin C will have the hook name C. Hooks are executed within the plugin by calling them via the associated I. $plugin->execute_plugin_hook( 'my_hook' ); You can also call any other hook if you provide the full name using the C method: $plugin->app->execute_hook( 'core.app.route_exception' ); Or using their alias: $plugin->app->execute_hook( 'on_route_exception' ); B If your plugin consumes a plugin that declares any hooks, those hooks are added to your application, even though DSL is not. =head2 Writing Test Gotchas =head3 Constructor for Dancer2::Plugin::Foo has been inlined and cannot be updated You'll usually get this one because you are defining both the plugin and app in your test file, and the runtime creation of Moo's attributes happens after the compile-time import voodoo dance. To get around this nightmare, wrap your plugin definition in a C block. BEGIN { package Dancer2::Plugin::Foo; use Dancer2::Plugin; has bar => ( is => 'ro', from_config => 1, ); plugin_keywords qw/ bar /; } { package MyApp; use Dancer2; use Dancer2::Plugin::Foo; bar(); } =head3 You cannot overwrite a locally defined method (bar) with a reader If you set an object attribute of your plugin to be a keyword as well, you need to call C after the attribute definition. package Dancer2::Plugin::Foo; use Dancer2::Plugin; has bar => ( is => 'ro', ); plugin_keywords 'bar'; =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2017 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut context-in-before.t100644001751001751 211713171470520 16673 0ustar00jasonjason000000000000Dancer2-0.205002/t#!perl use strict; use warnings; use Test::More tests => 10; use Plack::Test; use HTTP::Request::Common; my $before; { package OurApp; use Dancer2 '!pass'; use Test::More; hook before => sub { my $ctx = shift; isa_ok( $ctx, 'Dancer2::Core::App', 'Context is actually an app now', ); is( $ctx->name, 'OurApp', 'It is the correct app' ); can_ok( $ctx, 'app' ); my $app = $ctx->app; isa_ok( $app, 'Dancer2::Core::App', 'When called ->app, we get te app again', ); is( $app->name, 'OurApp', 'It is the correct app' ); is( $ctx, $app, 'Same exact application (by reference)' ); $before++; }; get '/' => sub {'OK'}; } my $app = OurApp->to_app; isa_ok( $app, 'CODE', 'Got app' ); test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/' ); is( $res->code, 200, '[GET /] status OK' ); is( $res->content, 'OK', '[GET /] content OK' ); ok( $before == 1, 'before hook called' ); }; session_lifecycle.t100644001751001751 1460213171470520 17067 0ustar00jasonjason000000000000Dancer2-0.205002/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use HTTP::Cookies; use lib 't/lib'; { package App; use Dancer2; set session => 'Simple'; set show_errors => 1; get '/no_session_data' => sub { return "session not modified"; }; get '/set_session/*' => sub { my ($name) = splat; session name => $name; }; get '/read_session' => sub { my $name = session('name') || ''; "name='$name'"; }; get '/change_session_id' => sub { app->change_session_id; }; get '/destroy_session' => sub { my $name = session('name') || ''; app->destroy_session; return "destroyed='$name'"; }; get '/churn_session' => sub { app->destroy_session; session name => 'damian'; return "churned"; }; } my $url = 'http://localhost'; my $test = Plack::Test->create( App->to_app ); my $app = Dancer2->runner->apps->[0]; for my $engine (qw(YAML Simple SimpleNoChangeId)) { # clear current session engine, and rebuild for the test # This is *really* messy, playing in object hashrefs.. delete $app->{session_engine}; $app->config->{session} = $engine; $app->session_engine; # trigger a build my $jar = HTTP::Cookies->new(); subtest "[$engine] No cookie set if session not referenced" => sub { my $res = $test->request(GET "$url/no_session_data"); ok $res->is_success, "/no_session_data" or diag explain $res; $jar->extract_cookies($res); ok(!$jar->as_string, 'No cookie set'); }; subtest "[$engine] No empty session created if session read attempted" => sub { my $res = $test->request(GET "$url/read_session"); ok $res->is_success, "/read_session"; $jar->extract_cookies($res); ok(!$jar->as_string, 'No cookie set'); }; my $sid1; subtest "[$engine] Set value into session" => sub { my $res = $test->request(GET "$url/set_session/larry"); ok $res->is_success, "/set_session/larry"; $jar->extract_cookies($res); ok($jar->as_string, 'Cookie set'); # extract SID $jar->scan(sub { $sid1 = $_[2] }); ok($sid1, 'Got SID from cookie'); }; subtest "[$engine] Read value back" => sub { # read value back my $req = GET "$url/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/read_session"; $jar->clear; ok(!$jar->as_string, 'Jar cleared'); $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set again'); like $res->content, qr/name='larry'/, "session value looks good"; }; subtest "[$engine] Session cookie persists even if we do not touch sessions" => sub { my $req = GET "$url/no_session_data"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/no_session_data"; $jar->clear; ok(!$jar->as_string, 'Jar cleared'); $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set again'); }; my $sid2; subtest "[$engine] Change session ID" => sub { my $req = GET "$url/change_session_id"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/change_session_id"; $jar->clear; ok(!$jar->as_string, 'Jar cleared'); $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set again'); # extract SID $jar->scan(sub { $sid2 = $_[2] }); isnt $sid2, $sid1, "New session has different ID"; is $res->content, $sid2, "new session ID returned"; }; subtest "[$engine] Read value back after change_session_id" => sub { # read value back my $req = GET "$url/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/read_session"; $jar->clear; ok(!$jar->as_string, 'Jar cleared'); $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set again'); like $res->content, qr/name='larry'/, "session value looks good"; }; subtest "[$engine] Destroy session and check that cookies expiration is set" => sub { my $req = GET "$url/destroy_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/destroy_session"; ok($jar->as_string, 'We have a cookie before reading response'); $jar->extract_cookies($res); ok(!$jar->as_string, 'Cookie was removed from jar'); }; subtest "[$engine] Session cookie not sent after session destruction" => sub { my $req = GET "$url/no_session_data"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/no_session_data"; ok(!$jar->as_string, 'Jar is empty'); $jar->extract_cookies($res); ok(!$jar->as_string, 'Jar still empty (no new session cookie)'); }; my $sid3; subtest "[$engine] Set value into session again" => sub { my $res = $test->request(GET "$url/set_session/curly"); ok $res->is_success, "/set_session/larry"; $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set'); # extract SID $jar->scan(sub { $sid3 = $_[2] }); isnt $sid3, $sid2, "New session has different ID"; }; subtest "[$engine] Destroy and create a session in one request" => sub { my $req = GET "$url/churn_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/churn_session"; $jar->extract_cookies($res); ok($jar->as_string, 'session cookie set'); my $sid4; $jar->scan(sub { $sid4 = $_[2] }); isnt $sid4, $sid3, "Changed session has different ID"; }; subtest "[$engine] Read value back" => sub { my $req = GET "$url/read_session"; $jar->add_cookie_header($req); my $res = $test->request($req); ok $res->is_success, "/read_session"; $jar->extract_cookies($res); ok($jar->as_string, "session cookie set"); like $res->content, qr/name='damian'/, "session value looks good"; }; } done_testing; route_retvals.t100644001751001751 63113171470520 17002 0ustar00jasonjason000000000000Dancer2-0.205002/t/dsluse strict; use warnings; use Dancer2; use Test::More (); my @routes = get '/' => sub {1}; Test::More::is( scalar @routes, 2, 'Two routes available' ); foreach my $route (@routes) { Test::More::isa_ok( $route, 'Dancer2::Core::Route' ); } Test::More::is( $routes[0]->method, 'get', 'Created GET route' ); Test::More::is( $routes[1]->method, 'head', 'Created HEAD route too' ); Test::More::done_testing; bin000755001751001751 013171470520 14671 5ustar00jasonjason000000000000Dancer2-0.205002/t/app/t1app.psgi100644001751001751 4713171470520 16436 0ustar00jasonjason000000000000Dancer2-0.205002/t/app/t1/bin#!perl use Dancer2; use App1; start; no-config.t100644001751001751 77313171470520 16570 0ustar00jasonjason000000000000Dancer2-0.205002/t/plugin2use strict; use warnings; use Test::More tests => 1; BEGIN { package Dancer2::Plugin::Foo; use Dancer2::Plugin; has bar => ( is => 'ro', from_config => 1, ); has baz => ( is => 'ro', default => sub { $_[0]->config->{baz} }, ); plugin_keywords qw/ bar baz /; } { package MyApp; use Dancer2; use Dancer2::Plugin::Foo; bar(); baz(); } pass "we survived bar() and baz()"; config.yml100644001751001751 263713171470520 16752 0ustar00jasonjason000000000000Dancer2-0.205002/share/skel# This is the main configuration file of your Dancer2 app # env-related settings should go to environments/$env.yml # all the settings in this file will be loaded at Dancer's startup. # Your application's name appname: "[d2% appname %2d]" # The default layout to use for your application (located in # views/layouts/main.tt) layout: "main" # when the charset is set to UTF-8 Dancer2 will handle for you # all the magic of encoding and decoding. You should not care # about unicode within your app when this setting is set (recommended). charset: "UTF-8" # template engine # simple: default and very basic template engine # template_toolkit: TT template: "simple" # template: "template_toolkit" # engines: # template: # template_toolkit: # start_tag: '<%' # end_tag: '%>' # session engine # # Simple: in-memory session store - Dancer2::Session::Simple # YAML: session stored in YAML files - Dancer2::Session::YAML # # Check out metacpan for other session storage options: # https://metacpan.org/search?q=Dancer2%3A%3ASession&search_type=modules # # Default value for 'cookie_name' is 'dancer.session'. If you run multiple # Dancer apps on the same host then you will need to make sure 'cookie_name' # is different for each app. # #engines: # session: # Simple: # cookie_name: testapp.session # #engines: # session: # YAML: # cookie_name: eshop.session # is_secure: 1 # is_http_only: 1 author-pod-syntax.t100644001751001751 45413171470520 16733 0ustar00jasonjason000000000000Dancer2-0.205002/t#!perl BEGIN { unless ($ENV{AUTHOR_TESTING}) { print qq{1..0 # SKIP these tests are for testing by the author\n}; exit } } # This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests. use strict; use warnings; use Test::More; use Test::Pod 1.41; all_pod_files_ok(); 00-report-prereqs.t100644001751001751 1342613171470520 16577 0ustar00jasonjason000000000000Dancer2-0.205002/t#!perl use strict; use warnings; # This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs 0.027 use Test::More tests => 1; use ExtUtils::MakeMaker; use File::Spec; # from $version::LAX my $lax_version_re = qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )? | (?:\.[0-9]+) (?:_[0-9]+)? ) | (?: v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )? | (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)? ) )/x; # hide optional CPAN::Meta modules from prereq scanner # and check if they are available my $cpan_meta = "CPAN::Meta"; my $cpan_meta_pre = "CPAN::Meta::Prereqs"; my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic # Verify requirements? my $DO_VERIFY_PREREQS = 1; sub _max { my $max = shift; $max = ( $_ > $max ) ? $_ : $max for @_; return $max; } sub _merge_prereqs { my ($collector, $prereqs) = @_; # CPAN::Meta::Prereqs object if (ref $collector eq $cpan_meta_pre) { return $collector->with_merged_prereqs( CPAN::Meta::Prereqs->new( $prereqs ) ); } # Raw hashrefs for my $phase ( keys %$prereqs ) { for my $type ( keys %{ $prereqs->{$phase} } ) { for my $module ( keys %{ $prereqs->{$phase}{$type} } ) { $collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module}; } } } return $collector; } my @include = qw( ); my @exclude = qw( ); # Add static prereqs to the included modules list my $static_prereqs = do './t/00-report-prereqs.dd'; # Merge all prereqs (either with ::Prereqs or a hashref) my $full_prereqs = _merge_prereqs( ( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ), $static_prereqs ); # Add dynamic prereqs to the included modules list (if we can) my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; my $cpan_meta_error; if ( $source && $HAS_CPAN_META && (my $meta = eval { CPAN::Meta->load_file($source) } ) ) { $full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs); } else { $cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source) $source = 'static metadata'; } my @full_reports; my @dep_errors; my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs; # Add static includes into a fake section for my $mod (@include) { $req_hash->{other}{modules}{$mod} = 0; } for my $phase ( qw(configure build test runtime develop other) ) { next unless $req_hash->{$phase}; next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING}); for my $type ( qw(requires recommends suggests conflicts modules) ) { next unless $req_hash->{$phase}{$type}; my $title = ucfirst($phase).' '.ucfirst($type); my @reports = [qw/Module Want Have/]; for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) { next if $mod eq 'perl'; next if grep { $_ eq $mod } @exclude; my $file = $mod; $file =~ s{::}{/}g; $file .= ".pm"; my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC; my $want = $req_hash->{$phase}{$type}{$mod}; $want = "undef" unless defined $want; $want = "any" if !$want && $want == 0; my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required"; if ($prefix) { my $have = MM->parse_version( File::Spec->catfile($prefix, $file) ); $have = "undef" unless defined $have; push @reports, [$mod, $want, $have]; if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) { if ( $have !~ /\A$lax_version_re\z/ ) { push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)"; } elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) { push @dep_errors, "$mod version '$have' is not in required range '$want'"; } } } else { push @reports, [$mod, $want, "missing"]; if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) { push @dep_errors, "$mod is not installed ($req_string)"; } } } if ( @reports ) { push @full_reports, "=== $title ===\n\n"; my $ml = _max( map { length $_->[0] } @reports ); my $wl = _max( map { length $_->[1] } @reports ); my $hl = _max( map { length $_->[2] } @reports ); if ($type eq 'modules') { splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl]; push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports; } else { splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl]; push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports; } push @full_reports, "\n"; } } } if ( @full_reports ) { diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports; } if ( $cpan_meta_error || @dep_errors ) { diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n"; } if ( $cpan_meta_error ) { my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n"; } if ( @dep_errors ) { diag join("\n", "\nThe following REQUIRED prerequisites were not satisfied:\n", @dep_errors, "\n" ); } pass; # vim: ts=4 sts=4 sw=4 et: Manual.pod100644001751001751 27107013171470520 16726 0ustar00jasonjason000000000000Dancer2-0.205002/lib/Dancer2# ABSTRACT: A gentle introduction to Dancer2 package Dancer2::Manual; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Manual - A gentle introduction to Dancer2 =head1 VERSION version 0.205002 =head1 DESCRIPTION Dancer2 is a free and open source web application framework written in Perl. It's a complete rewrite of L, based on L and using a more robust and extensible fully-OO design. It's designed to be powerful and flexible, but also easy to use - getting up and running with your web app is trivial, and an ecosystem of adaptors for common template engines, session storage, logging methods, serializers, and plugins to make common tasks easy means you can do what you want to do, your way, easily. =head1 INSTALL Installation of Dancer2 is simple: perl -MCPAN -e 'install Dancer2' Thanks to the magic of cpanminus, if you do not have CPAN.pm configured, or just want a quickfire way to get running, the following should work, at least on Unix-like systems: wget -O - http://cpanmin.us | sudo perl - Dancer2 (If you don't have root access, omit the 'sudo', and cpanminus will install Dancer2 and prereqs into C<~/perl5>.) =head1 BOOTSTRAPPING A NEW APP Create a web application using the dancer script: $ dancer2 -a MyApp && cd MyApp + MyApp + MyApp/config.yml + MyApp/Makefile.PL + MyApp/MANIFEST.SKIP + MyApp/.dancer + MyApp/cpanfile + MyApp/bin + MyApp/bin/app.psgi + MyApp/environments + MyApp/environments/development.yml + MyApp/environments/production.yml + MyApp/lib + MyApp/lib/MyApp.pm + MyApp/public + MyApp/public/favicon.ico + MyApp/public/500.html + MyApp/public/dispatch.cgi + MyApp/public/404.html + MyApp/public/dispatch.fcgi + MyApp/public/css + MyApp/public/css/error.css + MyApp/public/css/style.css + MyApp/public/images + MyApp/public/images/perldancer.jpg + MyApp/public/images/perldancer-bg.jpg + MyApp/public/javascripts + MyApp/public/javascripts/jquery.js + MyApp/t + MyApp/t/001_base.t + MyApp/t/002_index_route.t + MyApp/views + MyApp/views/index.tt + MyApp/views/layouts + MyApp/views/layouts/main.tt It creates a directory named after the name of the app, along with a configuration file, a views directory (where your templates and layouts will live), an environments directory (where environment-specific settings live), a module containing the actual guts of your application, and a script to start it. A default skeleton is used to bootstrap the new application, but you can use the C<-s> option to provide another skeleton. For example: $ dancer2 -a MyApp -s ~/mydancerskel For an example of a skeleton directory check the default one available in the C directory of your Dancer2 distribution. (In what follows we will refer to the directory in which you have created your Dancer2 application -- I what C was above -- as the C.) Because Dancer2 is a L web application framework, you can use the C tool (provided by L) for launching the application: plackup -p 5000 bin/app.psgi View the web application at: http://localhost:5000 =head1 USAGE When Dancer2 is imported to a script, that script becomes a webapp, and at this point, all the script has to do is declare a list of B. A route handler is composed by an HTTP method, a path pattern and a code block. C, C and C pragmas are also imported with Dancer2. The code block given to the route handler has to return a string which will be used as the content to render to the client. Routes are defined for a given HTTP method. For each method supported, a keyword is exported by the module. =head2 HTTP Methods Here are some of the standard HTTP methods which you can use to define your route handlers. =over 4 =item * B The GET method retrieves information, and is the most common GET requests should be used for typical "fetch" requests - retrieving information. They should not be used for requests which change data on the server or have other effects. When defining a route handler for the GET method, Dancer2 automatically defines a route handler for the HEAD method (in order to honour HEAD requests for each of your GET route handlers). To define a GET action, use the L keyword. =item * B The POST method is used to create a resource on the server. To define a POST action, use the L keyword. =item * B The PUT method is used to replace an existing resource. To define a PUT action, use the L keyword. a PUT request should replace the existing resource with that specified - for instance - if you wanted to just update an email address for a user, you'd have to specify all attributes of the user again; to make a partial update, a PATCH request is used. =item * B The PATCH method updates some attributes of an existing resource. To define a PATCH action, use the L keyword. =item * B The DELETE method requests that the origin server delete the resource identified by the Request-URI. To define a DELETE action, use the L keyword. =back =head3 Handling multiple HTTP request methods Routes can use C to match all, or a specified list of HTTP methods. The following will match any HTTP request to the path C: any '/myaction' => sub { # code } The following will match GET or POST requests to C: any ['get', 'post'] => '/myaction' => sub { # code }; For convenience, any route which matches GET requests will also match HEAD requests. =head2 Route Handlers The route action is the code reference declared. It can access parameters through the specific C, C, and C keywords, which return a L object. This hashref is a merge of the route pattern matches and the request params. You can have more details about how params are built and how to access them in the L documentation. =head3 Declaring Routes To control what happens when a web request is received by your webapp, you'll need to declare C. A route declaration indicates which HTTP method(s) it is valid for, the path it matches (e.g. C), and a coderef to execute, which returns the response. get '/hello/:name' => sub { return "Hi there " . route_parameters->get('name'); }; The above route specifies that, for GET requests to C, the code block provided should be executed. =head3 Retrieving request parameters The L, L, and L keywords provide a L result from the three different parameters. =head3 Named matching A route pattern can contain one or more tokens (a word prefixed with ':'). Each token found in a route pattern is used as a named-pattern match. Any match will be set in the route parameters. get '/hello/:name' => sub { "Hey " . route_parameters->get('name') . ", welcome here!"; }; Tokens can be optional, for example: get '/hello/:name?' => sub { my $name = route_parameters->get('name') //= 'Whoever you are'; "Hello there, $name"; }; =head3 Wildcard Matching A route can contain a wildcard (represented by a C<*>). Each wildcard match will be placed in a list, which the C keyword returns. get '/download/*.*' => sub { my ($file, $ext) = splat; # do something with $file.$ext here }; An extensive, greedier wildcard represented by C<**> (A.K.A. "megasplat") can be used to define a route. The additional path is broken down and returned as an arrayref: get '/entry/*/tags/**' => sub { my ( $entry_id, $tags ) = splat; my @tags = @{$tags}; }; The C keyword in the above example for the route F would set C<$entry_id> to C<1> and C<$tags> to C<['one', 'two']>. =head3 Mixed named and wildcard matching A route can combine named (token) matching and wildcard matching. This is useful when chaining actions: get '/team/:team/**' => sub { var team => route_parameters->get('team'); pass; }; prefix '/team/:team'; get '/player/*' => sub { my ($player) = splat; # etc... }; get '/score' => sub { return score_for( vars->{'team'} ); }; =head3 Regular Expression Matching A route can be defined with a Perl regular expression. In order to tell Dancer2 to consider the route as a real regexp, the route must be defined explicitly with C, like the following: get qr{/hello/([\w]+)} => sub { my ($name) = splat; return "Hello $name"; }; For Perl 5.10+, a route regex may use named capture groups. The C keyword will return a reference to a copy of C<%+>. =head3 Conditional Matching Routes may include some matching conditions (on content_type, agent, user_agent, content_length and path_info): get '/foo', {agent => 'Songbird (\d\.\d)[\d\/]*?'} => sub { 'foo method for songbird' } get '/foo' => sub { 'all browsers except songbird' } =head2 Prefix A prefix can be defined for each route handler, like this: prefix '/home'; From here, any route handler is defined to /home/* get '/page1' => sub {}; # will match '/home/page1' You can unset the prefix value prefix '/'; # or: prefix undef; get '/page1' => sub {}; # will match /page1 Alternatively, to prevent you from ever forgetting to undef the prefix, you can use lexical prefix like this: prefix '/home' => sub { get '/page1' => sub {}; # will match '/home/page1' }; ## prefix reset to previous value on exit get '/page1' => sub {}; # will match /page1 =head2 Delayed responses (Async/Streaming) L can provide delayed (otherwise known as I) responses using the C keyword. These responses are streamed, although you can set the content all at once, if you prefer. get '/status' => sub { delayed { response_header 'X-Foo' => 'Bar'; # flush headers (in case of streaming) flush; # send content to the user content 'Hello, world!'; # you can write more content # all streaming content 'Hello, again!'; # when done, close the connection done; # do whatever you want else, asynchronously # the user socket closed by now ... }; }; If you are streaming (calling C several times), you must call C first. If you're sending only once, you don't need to call C. Here is an example of using delayed responses with L: use Dancer2; use AnyEvent; my %timers; my $count = 5; get '/drums' => sub { delayed { print "Stretching...\n"; flush; # necessary, since we're streaming $timers{'Snare'} = AE::timer 1, 1, delayed { $timers{'HiHat'} ||= AE::timer 0, 0.5, delayed { content "Tss...\n"; }; content "Bap!\n"; if ( $count-- == 0 ) { %timers = (); content "Tugu tugu tugu dum!\n"; done; print "\n\n"; $timers{'Applause'} = AE::timer 3, 0, sub { # the DSL will not available here # because we didn't call the "delayed" keyword print "\n"; }; } }; }; }; If an error happens during a write operation, a warning will be issued to the logger. You can handle the error yourself by providing an C handler: get '/' => sub { delayed { flush; content "works"; # ... user disconnected here ... content "fails"; # ... error triggered ... done; # doesn't even get run } on_error => sub { # delayed{} not needed, DSL already available my ($error) = @_; # do something with $error }; }; Here is an example that asynchronously streams the contents of a CSV file: use Dancer2; use Text::CSV_XS qw< csv >; use Path::Tiny qw< path >; use JSON::MaybeXS qw< encode_json >; # Create CSV parser my $csv = Text::CSV_XS->new({ binary => 1, auto_diag => 1, }); get '/' => sub { # delayed response: delayed { # streaming content flush; # Read each row and stream it in JSON my $fh = path('filename.csv')->openr_utf8; while ( my $row = $csv->getline($fh) ) { content encode_json $row; } # close user connection done; } on_error => sub { my ($error) = @_; warning 'Failed to stream to user: ' . request->remote_address; }; }; B If you just want to send a file's contents asynchronously, use C instead of C, as it will automatically take advantage of any asynchronous capability. =head2 Action Skipping An action can choose not to serve the current request and ask Dancer2 to process the request with the next matching route. This is done with the B keyword, like in the following example get '/say/:word' => sub { pass if route_parameters->get('word') =~ /^\d+$/; "I say a word: " . route_parameters->get('word'); }; get '/say/:number' => sub { "I say a number: " . route_parameters->get('number'); }; =head1 HOOKS Hooks are code references (or anonymous subroutines) that are triggered at specific moments during the resolution of a request. Many of them are supported by the core but plugins and engines can also define their own. =over 4 =item * C hooks C hooks are evaluated before each request within the context of the request and receives as argument the app (a L object). It's possible to define variables which will be accessible in the action blocks with the keyword C. hook before => sub { var note => 'Hi there'; }; get '/foo/*' => sub { my ($match) = splat; # 'oversee'; vars->{note}; # 'Hi there' }; For another example, this can be used along with session support to easily give non-logged-in users a login page: hook before => sub { if (!session('user') && request->path !~ m{^/login}) { # Pass the original path requested along to the handler: forward '/login', { requested_path => request->path }; } }; The request keyword returns the current L object representing the incoming request. =item * C hooks C hooks are evaluated after the response has been built by a route handler, and can alter the response itself, just before it's sent to the client. This hook runs after a request has been processed, but before the response is sent. It receives a L object, which it can modify if it needs to make changes to the response which is about to be sent. The hook can use other keywords in order to do whatever it wants. hook after => sub { response->content( q{The "after" hook can alter the response's content here!} ); }; =back =head2 Templates =over 4 =item * C C hooks are called whenever a template is going to be processed, they are passed the tokens hash which they can alter. hook before_template_render => sub { my $tokens = shift; $tokens->{foo} = 'bar'; }; The tokens hash will then be passed to the template with all the modifications performed by the hook. This is a good way to setup some global vars you like to have in all your templates, like the name of the user logged in or a section name. =item * C C hooks are called after the view has been rendered. They receive as their first argument the reference to the content that has been produced. This can be used to post-process the content rendered by the template engine. hook after_template_render => sub { my $ref_content = shift; my $content = ${$ref_content}; # do something with $content ${$ref_content} = $content; }; =item * C C hooks are called whenever the layout is going to be applied to the current content. The arguments received by the hook are the current tokens hashref and a reference to the current content. hook before_layout_render => sub { my ($tokens, $ref_content) = @_; $tokens->{new_stuff} = 42; $ref_content = \"new content"; }; =item * C C hooks are called once the complete content of the view has been produced, after the layout has been applied to the content. The argument received by the hook is a reference to the complete content string. hook after_layout_render => sub { my $ref_content = shift; # do something with ${ $ref_content }, which reflects directly # in the caller }; =back =head2 Error Handling Refer to L for details about the following hooks: =over 4 =item * C =item * C =item * C =item * C =back =head2 File Rendering Refer to L for details on the following hooks: =over 4 =item * C =item * C =back =head2 Serializers =over 4 =item * C is called before serializing the content, and receives the content to serialize as an argument. hook before_serializer => sub { my $content = shift; ... }; =item * C is called after the payload has been serialized, and receives the serialized content as an argument. hook after_serializer => sub { my $serialized_content = shift; ... }; =back =head1 HANDLERS =head2 File Handler Whenever a content is produced out of the parsing of a static file, the L component is used. This component provides two hooks, C and C. C hooks are called just before starting to parse the file, the hook receives as its first argument the file path that is going to be processed. hook before_file_render => sub { my $path = shift; }; C hooks are called after the file has been parsed and the response content produced. It receives the response object (L) produced. hook after_file_render => sub { my $response = shift; }; =head2 Auto page Whenever a page that matches an existing template needs to be served, the L component is used. =head2 Writing your own A route handler is a class that consumes the L role. The class must implement a set of methods: C, C and C which will be used to declare the route. Let's look at L for example. First, the matching methods are C and C: sub methods { qw(head get) } Then, the C or the I we want to match: sub regexp { '/:page' } Anything will be matched by this route, since we want to check if there's a view named with the value of the C token. If not, the route needs to C, letting the dispatching flow to proceed further. sub code { sub { my $app = shift; my $prefix = shift; my $template = $app->template_engine; if ( !defined $template ) { $app->response->has_passed(1); return; } my $page = $app->request->path; my $layout_dir = $template->layout_dir; if ( $page =~ m{^/\Q$layout_dir\E/} ) { $app->response->has_passed(1); return; } # remove leading '/', ensuring paths relative to the view $page =~ s{^/}{}; my $view_path = $template->view_pathname($page); if ( ! $template->pathname_exists( $view_path ) ) { $app->response->has_passed(1); return; } my $ct = $template->process( $page ); return ( $app->request->method eq 'GET' ) ? $ct : ''; }; } The C method passed the L object which provides access to anything needed to process the request. A C is then implemented to add the route to the registry and if the C is off, it does nothing. sub register { my ($self, $app) = @_; return unless $app->config->{auto_page}; $app->add_route( method => $_, regexp => $self->regexp, code => $self->code, ) for $self->methods; } The config parser looks for a C section and any handler defined there is loaded. Thus, any random handler can be added to your app. For example, the default config file for any Dancer2 application is as follows: route_handlers: File: public_dir: /path/to/public AutoPage: 1 =head1 ERRORS =head2 Error Pages When an HTTP error occurs (i.e. the action responds with a status code other than 200), this is how Dancer2 determines what page to display. =over =item * Looks in the C directory for a corresponding template file matching the error code (e.g. C<500.tt> or C<404.tt>). If such a file exists, it's used to report the error. =item * Next, looks in the C directory for a corresponding HTML file matching the error code (e.g. C<500.html> or C<404.html>). If such a file exists, it's used to report the error. (Note, however, that if B is set to true, in the case of a 500 error the static HTML page will not be shown, but will be replaced with a default error page containing more informative diagnostics. For more information see L.) =item * As default, render a generic error page on the fly. =back =head2 Execution Errors When an error occurs during the route execution, Dancer2 will render an error page with the HTTP status code 500. It's possible either to display the content of the error message or to hide it with a generic error page. This is a choice left to the end-user and can be controlled with the B setting (see above). Note that you can also choose to consider all warnings in your route handlers as errors when the setting B is set to 1. =head2 Error Hooks When an error is caught by Dancer2's core, an exception object is built (of the class L). This class provides a hook to let the user alter the error workflow if needed. C hooks are called whenever an error object is built, the object is passed to the hook. hook init_error => sub { my $error = shift; # do something with $error }; I in Dancer, both names currently are synonyms for backward-compatibility.> C hooks are called whenever an error is going to be thrown, it receives the error object as its sole argument. hook before_error => sub { my $error = shift; # do something with $error }; I in Dancer, both names currently are synonyms for backward-compatibility.> C hooks are called whenever an error object has been thrown, it receives a L object as its sole argument. hook after_error => sub { my $response = shift; }; I in Dancer, both names currently are synonyms for backward-compatibility.> C is called when an exception has been caught, at the route level, just before rethrowing it higher. This hook receives a L and the error as arguments. hook on_route_exception => sub { my ($app, $error) = @_; }; =head1 SESSIONS =head2 Handling sessions It's common to want to use sessions to give your web applications state; for instance, allowing a user to log in, creating a session, and checking that session on subsequent requests. By default Dancer 2 has L sessions enabled. It implements a very simple in-memory session storage. This will be fast and useful for testing, but such sessions will not persist between restarts of your app. If you'd like to use a different session engine you must declare it in the configuration file. For example to use YAML file base sessions you need to add the following to your F: session: YAML Or, to enable session support from within your code, set session => 'YAML'; (However, controlling settings is best done from your config file.) The L backend implements a file-based YAML session storage to help with debugging, but shouldn't be used on production systems. There are other session backends, such as L, which are recommended for production use. You can then use the L keyword to manipulate the session: =head3 Storing data in the session Storing data in the session is as easy as: session varname => 'value'; =head3 Retrieving data from the session Retrieving data from the session is as easy as: session('varname') Or, alternatively, session->read("varname") =head3 Controlling where sessions are stored For disc-based session backends like L, L etc., session files are written to the session dir specified by the C setting, which defaults to C<./sessions> if not specifically set. If you need to control where session files are created, you can do so quickly and easily within your config file, for example: session: YAML engines: session: YAML: session_dir: /tmp/dancer-sessions If the directory you specify does not exist, Dancer2 will attempt to create it for you. =head3 Changing session ID If you wish to change the session ID (for example on privilege level change): my $new_session_id = app->change_session_id =head3 Destroying a session When you're done with your session, you can destroy it: app->destroy_session =head2 Sessions and logging in A common requirement is to check the user is logged in, and, if not, require them to log in before continuing. This can easily be handled using a before hook to check their session: use Dancer2; set session => "Simple"; hook before => sub { if (!session('user') && request->path !~ m{^/login}) { forward '/login', { requested_path => request->path }; } }; get '/' => sub { return "Home Page"; }; get '/secret' => sub { return "Top Secret Stuff here"; }; get '/login' => sub { # Display a login page; the original URL they requested is available as # query_parameters->get('requested_path'), so could be put in a hidden field in the form template 'login', { path => query_parameters->get('requested_path') }; }; post '/login' => sub { # Validate the username and password they supplied if (body_parameters->get('user') eq 'bob' && body_parameters->get('pass') eq 'letmein') { session user => body_parameters->get('user'); redirect body_parameters->get('path') || '/'; } else { redirect '/login?failed=1'; } }; dance(); Here is what the corresponding C file should look like. You should place it in a directory called C: Session and logging in
User Name : Password:
Of course, you'll probably want to validate your users against a database table, or maybe via IMAP/LDAP/SSH/POP3/local system accounts via PAM etc. L is probably a good starting point here! A simple working example of handling authentication against a database table yourself (using L which provides the C keyword, and L to handle salted hashed passwords (well, you wouldn't store your users passwords in the clear, would you?)) follows: post '/login' => sub { my $user_value = body_parameters->get('user'); my $pass_value = body_parameters->get('pass'); my $user = database->quick_select('users', { username => $user_value } ); if (!$user) { warning "Failed login for unrecognised user $user_value"; redirect '/login?failed=1'; } else { if (Crypt::SaltedHash->validate($user->{password}, $pass_value)) { debug "Password correct"; # Logged in successfully session user => $user; redirect body_parameters->get('path') || '/'; } else { debug("Login failed - password incorrect for " . $user_value); redirect '/login?failed=1'; } } }; =head3 Retrieve complete hash stored in session Get complete hash stored in session: my $hash = session; =head2 Writing a session engine In Dancer 2, a session backend consumes the role L. The following example using the Reddis session demonstrates how session engines are written in Dancer 2. First thing to do is to create the class for the session engine, we'll name it C: package Dancer2::Session::Redis; use Moo; with 'Dancer2::Core::Role::SessionFactory'; we want our backend to have a handle over a Redis connection. To do that, we'll create an attribute C use JSON; use Redis; use Dancer2::Core::Types; # brings helper for types has redis => ( is => 'rw', isa => InstanceOf['Redis'], lazy => 1, builder => '_build_redis', ); The lazy attribute says to Moo that this attribute will be built (initialized) only when called the first time. It means that the connection to Redis won't be opened until necessary. sub _build_redis { my ($self) = @_; Redis->new( server => $self->server, password => $self->password, encoding => undef, ); } Two more attributes, C and C need to be created. We do this by defining them in the config file. Dancer2 passes anything defined in the config to the engine creation. # config.yml ... engines: session: Redis: server: foo.mydomain.com password: S3Cr3t The server and password entries are now passed to the constructor of the Redis session engine and can be accessed from there. has server => (is => 'ro', required => 1); has password => (is => 'ro'); Next, we define the subroutine C<_retrieve> which will return a session object for a session ID it has passed. Since in this case, sessions are going to be stored in Redis, the session ID will be the key, the session the value. So retrieving is as easy as doing a get and decoding the JSON string returned: sub _retrieve { my ($self, $session_id) = @_; my $json = $self->redis->get($session_id); my $hash = from_json( $json ); return bless $hash, 'Dancer2::Core::Session'; } The C<_flush> method is called by Dancer when the session needs to be stored in the backend. That is actually a write to Redis. The method receives a C object and is supposed to store it. sub _flush { my ($self, $session) = @_; my $json = encode_json( { %{ $session } } ); $self->redis->set($session->id, $json); } For the C<_destroy> method which is supposed to remove a session from the backend, deleting the key from Redis is enough. sub _destroy { my ($self, $session_id) = @_; $self->redis->del($session_id); } The C<_sessions> method which is supposed to list all the session IDs currently stored in the backend is done by listing all the keys that Redis has. sub _sessions { my ($self) = @_; my @keys = $self->redis->keys('*'); return \@keys; } The session engine is now ready. =head3 The Session keyword Dancer2 maintains two session layers. The first layer, L provides a session object which represents the current session. You can read from it as many times as you want, and write to it as many times as you want. The second layer is the session engine (L is one example), which is used in order to implement the reading and writing from the actual storage. This is read only once, when a request comes in (using a cookie whose value is C by default). At the end of a request, all the data you've written will be flushed to the engine itself, which will do the actual write to the storage (whether it's in a hash in memory, in Memcache, or in a database). =head1 TEMPLATES Returning plain content is all well and good for examples or trivial apps, but soon you'll want to use templates to maintain separation between your code and your content. Dancer2 makes this easy. Your route handlers can use the L keyword to render templates. =head2 Views In Dancer2, a file which holds a template is called a I. Views are located in the C directory. You can change this location by changing the setting 'views'. For instance if your templates are located in the 'templates' directory, do the following: set views => path( app->location , 'templates' ); By default, the internal template engine L is used, but you may want to upgrade to L