Dancer2-2.1.0000755000765000024 015154413402 12365 5ustar00jasonstaff000000000000LICENSE100644000765000024 4643515154413402 13507 0ustar00jasonstaff000000000000Dancer2-2.1.0This software is copyright (c) 2024 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) 2024 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 Perl Artistic License 1.0 --- This software is Copyright (c) 2024 by Alexis Sukrieh. This is free software, licensed under: The Perl 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 as specified below. "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 uunet.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) give non-standard executables non-standard names, and clearly document 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. You may embed this Package's interpreter within an executable of yours (by linking); this shall be construed as a mere form of aggregation, provided that the complete Standard Version of the interpreter is so embedded. 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 whoever generated them, and may be sold commercially, and may be aggregated with this Package. If such scripts or library files are aggregated with this Package via the so-called "undump" or "unexec" methods of producing a binary executable image, then distribution of such an image shall neither be construed as a distribution of this Package nor shall it fall under the restrictions of Paragraphs 3 and 4, provided that you do not represent such an executable image as a Standard Version of this Package. 7. C subroutines (or comparably compiled subroutines in other languages) supplied by you and linked into this Package in order to emulate subroutines and variables of the language defined by this Package shall not be considered part of this Package, but are the equivalent of input as in Paragraph 6, provided these subroutines do not change the language in any way that would cause it to fail the regression tests for the language. 8. Aggregation of this Package with a commercial distribution is always permitted provided that the use of this Package is embedded; that is, when no overt attempt is made to make this Package's interfaces visible to the end user of the commercial distribution. Such use shall not be construed as a distribution of this Package. 9. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End AUTHORS100644000765000024 13115154413402 13471 0ustar00jasonstaff000000000000Dancer2-2.1.0See perldoc Dancer2.pm, section AUTHORS, for a list of core developers and contributors. Changes100644000765000024 24646215154413402 14017 0ustar00jasonstaff000000000000Dancer2-2.1.02.1.0 2026-03-11 21:29:26-04:00 America/New_York [ BUG FIXES ] * GH #686: Fix: to_json is double encoding UTF8 (Sawyer X) * GH #863: Fix case insensitive system confusion (Sawyer X) * GH #1124: Fix: charset config option is mostly ignored (Sawyer X) * GH #1143: Fix utf8 in URL (Sawyer X, Sorin Pop) * GH #1449, 1630: Make plugin DSL keyword app-specific (Sawyer X) * GH #1772: t/dsl/send_file.t fails with content_type-related errors (Sawyer X) * GH #1773: t/dsl/send_as.t throws json-related warnings (Jason A. Crome) * GH #1774: t/hooks.t throws void warnings (Jason A. Crome) * GH #1777: Properly unlink file uploads on Windows (Sawyer X) [ ENHANCEMENTS ] * GH #763: Strict config mode; warn on unknown config keys with opt-out (Sawyer X) * GH #763: Default strict config to off, but scaffold new apps with stict config enabled (Jason A. Crome) * GH #1073: Get multiple session cookie values at once with clear method (Sawyer X) * GH #1264: Move to Path::Tiny (Sawyer X) * GH #1323: Allow fully qualified namespaces for all engines (Russell @veryrusty Jenkins) * GH #1594: Use Unicode::UTF8 if available (Sawyer X) * GH #1664: Stop sending double server headers (Sawyer X) * GH #1709: `send_as` should use the full serializer, including hooks (Sawyer X) * PR #1757: Remove api_version, improve dispatching loop (Sawyer X) * PR #1758: Move MIME ownership to app (Sawyer X) * PR #1767: Turn off strict_config for noisy tests (Jason A. Crome) * PR #1780: Add package name capability to logger output (Mikko Koivunalho) [ DOCUMENTATION ] * GH #1431: Better document behavior of views setting (Jason A. Crome) * PR #1749, #1750: Fix broken manual and tutorial links (Gil Magno, Jason A. Crome) * PR #1753: Fix structure of config docs (Mikko Koivunalho) * PR #1762: Remove keyword logger from DSL document (Mikko Koivunalho) [ DEPRECATED ] * None [ MISC ] * PR #1776: Remove "Powered by..." from the error page (Jason A. Crome) 2.0.1 2025-10-22 18:13:37-04:00 America/New_York [ BUG FIXES ] * None [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * GH #1745: Fix cookbook link in README (icyavocado) * PR #1748: Various documentation fixes (Jason A. Crome) [ DEPRECATED ] * None [ MISC ] * None 2.0.0 2025-09-15 17:49:26-04:00 America/New_York [ BUG FIXES ] * GH #1701: Split cookie values on & only (Yanick Champoux) [ ENHANCEMENTS ] * GH #530: Make data censoring configurable (Yanick Champoux, David Precious) * GH #850: Scaffold tutorial app; allow multiple apps to be scaffolded in core Dancer2 (Jason A. Crome) * GH #1512: Log hook entries as they are executed (Yanick Champoux) * GH #1615: Remove Dancer2::Template::Simple from Dancer2 core (Jason A. Crome) * PR #1637: New, extendable configuration system (Mikko Koivunalho) * GH #1723: Enable use of a different Template Toolkit base class (Andy Beverley) * PR #1727: Don't create CPAN package files when generating new apps (Jason A. Crome) * PR #1731: Retire Template::Tiny fork, use CPAN's (Jason A. Crome, Karen Etheridge, Damien Krotkine, Yanick Champoux) * PR #1736: Allow config system to bootstrap itself (Yanick Champoux) * GH #1737: Add on_hook_exception for errors during hook processing (Andy Beverley) * PR #1739: Add source to on_hook_exception (Andy Beverley) * PR #1742: Refactor CLI for future expansion (Jason A. Crome) [ DOCUMENTATION ] * GH #1342: Document skipping private methods in pod coverage tests (Jason A. Crome) * PR #1721: New Dancer2 docs, reorganization of all documentation; from TPRF grant (Jason A. Crome) * PR #1741: Cover items missed in earlier documentation branch (Jason A. Crome) [ DEPRECATED ] * None [ MISC ] * None 1.1.2 2024-11-25 08:34:51-05:00 America/New_York [ BUG FIXES ] * None [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * None [ DEPRECATED ] * None [ MISC ] * PR #1715: Update deprecated download-artifact (dependabot) * GH #1716: Remove Scope::Upper from list of recommended deps (Russell @veryrusty Jenkins) * PR #1718: Add Module::Pluggable as a requirement, excluding broken versions (Russell @veryrusty Jenkins) * PR #1719: Force install of Module::Pluggable so CI can successfully run (Jason A. Crome, Russell @veryrusty Jenkins) 1.1.1 2024-07-18 19:48:00-04:00 America/New_York [ BUG FIXES ] * GH #1712: Fix use of send_as in templates (Andy Beverley) [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * PR #1706: Document missing logging hooks and log format option; fix typo in logging test (Jason A. Crome) [ DEPRECATED ] * None [ MISC ] * None 1.1.0 2023-12-11 20:28:26-05:00 America/New_York [ BUG FIXES ] * None [ ENHANCEMENTS ] * GH #33: Named routes; add uri_for_route keyword (Sawyer X) [ DOCUMENTATION ] * None [ DEPRECATED ] * None [ MISC ] * None 1.0.0 2023-10-09 10:10:10-04:00 America/New_York [ BUG FIXES ] * GH #1663: Allow overriding of prefix in add_route (GeekRuthie) * GH #1675: Stringify VERSION_FROM correctly in Makefile.PL (Jason A. Crome) * GH #1677: Don't deserialize multipart form data on post (Emil Perhinschi) * GH #1694: Update JS assets in Dancer2 app skel (Jason A. Crome) [ ENHANCEMENTS ] * PR #1682: Bump minimum version of Perl to 5.14 (Jason A. Crome) [ DOCUMENTATION ] * GH #1580: Document the purpose of the .dancer file (Jason A. Crome) * GH #1669: Show correct usage of Dancer2::Core::Error (GeekRuthie) * GH #1674: Fix POD for input_handle() (mauke) * GH #1414: Add documentation resources to the doc map (Jason A. Crome, Yanick Champoux) * PR #1684: Remove shumphrey from core developers (Steven Humphrey) * GH #1685: Document the versioning scheme and Dancer2 release process (Jason A. Crome) * PR #1688: Fixed various bugs/issues in Dancer2 Pod (Jason A. Crome) * PR #1691: Update the contribution guidelines (Jason A. Crome) * PR #1692: Change README extension .mkdn -> .md (Jason A. Crome) [ DEPRECATED ] * GH #1645: Deprecated Dancer2::Test (Jason A. Crome) * GH #1646: Deprecated keyword: push_header (Jason A. Crome) * GH #1647: Deprecated keyword: header (Jason A. Crome) * GH #1648: Deprecated keyword: headers (Jason A. Crome) * GH #1649: Deprecated keyword: context (Jason A. Crome) * GH #1650: Deprecated: splat/capture named placeholders (Jason A. Crome) * GH #1651: Deprecated core Request instance method: request->dispatch_path (Jason A. Crome) * GH #1652: Deprecated keyword in plugins: plugin_setting (Jason A. Crome) * GH #1653: Deprecated keyword in plugins: dancer_app (Jason A. Crome) * GH #1654: Deprecated keyword in plugins: request (Jason A. Crome) * GH #1655: Deprecated keyword in plugins: var (Jason A. Crome) * GH #1656: Deprecated keyword in plugins: hook (Jason A. Crome) [ MISC ] * GH #1659: Rename `master` branch to be `main` (Yanick Champoux) 0.400001 2023-02-05 18:41:48-05:00 America/New_York [ BUG FIXES ] * PR #1247: Fix edge case in plugin compat (Sawyer X) * GH #1621: Fix recursion error in TT after longjump (Andy Beverley, Russell @veryrusty Jenkins) * PR #1667: Remove failing module from GitHub Actions config (Jason A. Crome) [ ENHANCEMENTS ] * GH #769, PR #829, #1662: Rename show_errors as show_stacktrace (Nuno Ramos Carvalho, Sawyer X) * GH #1594: Use Unicode::UTF8 if available (Sawyer X) [ DOCUMENTATION ] * GH #1657: Fix the Dancer2::DeprecationPolicy abstract (Jason A. Crome) * GH #1661: Add 'gen' command to 'dancer2' runs (Steve Bertrand) * PR #1671: Fix broken links in POD; avoid passive voice (Tom Hukins) 0.400000 2022-03-13 22:16:13-04:00 America/New_York [ BUG FIXES ] * PR #1634: Fix CI push setting to run tests on branches with / in the name (Stefan Hornburg - Racke) * PR #1641: Fix uninitialized warnings from parsing routes with mixed regex/splats (Russell @veryrusty Jenkins) [ ENHANCEMENTS ] * PR #1627: Set minimum Perl to 5.10.1 (Peter Mottram - SysPete) * PR #1643: Set minimum Perl to 5.12.5 (Jason A. Crome) [ DOCUMENTATION ] * PR #1633: Fix YAML example in the tutorial (Tina Müller) * PR #1644: Add deprecation policy to docs; link git guide (Jason A. Crome) 0.301004 2021-06-06 13:30:28-04:00 America/New_York [ BUG FIXES ] * GH #1624: Fix missing gen subcommand in tutorial (racke) * PR #1626: Add File::Which to Dancer2 dependencies (Jason A. Crome) [ ENHANCEMENTS ] * None [ DOCUMENTATION ] * None 0.301003 2021-06-03 09:24:33-04:00 America/New_York [ BUG FIXES ] * GH #1611: Redirect '/' doesn't always work as expected (Russell @veryrusty Jenkins, Christopher Gurnee) * PR #1620: Quiet spammy failing CI builds (Jason A. Crome) * PR #1623: Copy Dockerfile from the right spot (Jason A. Crome) [ ENHANCEMENTS ] * PR #1613: Add git features to Dancer2 CLI (Jason A. Crome) * PR #1614: Generate Dockerfile when creating new app (Jason A. Crome) [ DOCUMENTATION ] * PR #1563: Fix typos in perlcritic.rc notes (Achyut Kumar Panda) * PR #1609: Document and test for missing DSL keywords (racke, Jason A. Crome) * PR #1618: Provide a consistent list of community resources (Jason A. Crome) * PR #1619: Clarify Dancer2::Template::Simple's role in life (Jason A. Crome) 0.301002 2021-04-18 15:29:32-04:00 America/New_York [ BUG FIXES ] * None [ ENHANCEMENTS ] * GH #1593: Log files used to build config (Nick Tonkin) * PR #1608: Migrate CLI to CLI::Osprey (Jason A. Crome) # PR #1610: Replace File::Find with Path::Tiny in CLI (Jason A. Crome) [ DOCUMENTATION ] * PR #1597: Update Core/Hook.pm Pod (Paul Clements) 0.301001 2021-03-17 08:52:34-04:00 America/New_York [ BUG FIXES ] * PR #1607: Fix broken tests following App::Cmd removal (Jason Crome) [ ENHANCEMENTS ] * PR #1606: Clean up various build prereqs (Graham Knop) [ DOCUMENTATION ] * None 0.301000 2021-03-15 18:58:17-04:00 America/New_York [ BUG FIXES ] * PR #1586: Run version checks against MetaCPAN (Jason A. Crome) * PR #1604: Remove App::Cmd dependency; have script/dancer2 bail if it's not installed (David Precious) [ ENHANCEMENTS ] * GH #1064: Add DSL keyword request_data (Mickey Nasriachi) * PR #1581: Disable scheduled GitHub action on Forks (Gabor Szabo) * PR #1590: Improve install/documentation of XS modules (Jason A. Crome) * PR #1591: Add more plugins to GitHub Actions CI (Gabor Szabo) [ DOCUMENTATION ] * GH #1582: Small fixes in Migration POD (Sergiy Borodych) * PR #1595: Add cookbook recipe for enabling/disabling routes at runtime (Nick Tonkin) * PR #1599: Punctuation correction in docs (Nick Tonkin) 0.300005 2021-01-26 15:57:41-05:00 America/New_York [ BUG FIXES ] * GH #1546: Add MIME type for all files served from public (Russell @veryrusty Jenkins) * GH #1555: Remove all leftovers of warnings config setting (Sergiy Borodych) * GH #1557: Remove HTTP::XSCookies workaround (Alexander Pankoff) * GH #1564: Add script_name to redirects beginning with / (Nigel Gregoire) * GH #1567: Fix CSS so errors do not display ourside of
 HTML
      element (Elliot Holden)
    * GH #1568: Serializer::Mutable doesn't auto-load other serializers
      (Russell @veryrusty Jenkins)
    * GH #1579: Fix missing push_header method in Response::Delayed 
      (Paul Clements)
    
    [ ENHANCEMENTS ]
    * GH #1552: Update jQuery (Sergiy Borodych)
    * GH #1558: Test to make sure uploads aren't discarded after the
      forward keyword is used (Alexander Pankoff)
    * GH #1571: Add GitHub Actions support (Gabor Szabo)
    * GH #1572: Install Dancer2::Session::Cookie in order to run the test 
      t/issues/gh-811.t (racke)

    [ DOCUMENTATION ]
    * GH #1490: Document Dancer2::Core::App->template() (Steve Dondley)
    * GH #1551: Fix pod for cookie same site attribute (Sergiy Borodych)
    * GH #1562: Fix links, missing code in Tutorial (cloveistaken)

0.300004  2020-05-26 20:52:34-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1509: Request instantiation fails throw 400 Bad Response (Russell
      @veryrusty Jenkins). This resolves GH #1056, 1482, 1496, 1507, 1508,
      and 1510.
    
    [ ENHANCEMENTS ]
    * GH #1510: Test for proper multi-part form handing (ice-lenor, Sawyer X)
    * GH #1547: Cookie SameSite support (Russell @veryrusty Jenkins)

    [ DOCUMENTATION ]
    * None

0.300003  2020-04-09 10:39:55-04:00 America/New_York

    [ BUG FIXES ]
    * None
    
    [ ENHANCEMENTS ]
    * None

    [ DOCUMENTATION ]
    * GH #1543: Various tutorial cleanups (Peter Mottram - SysPete)

0.300002  2020-04-07 11:44:59-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1541: Fix broken test that is skipped under CI (Peter Mottram - 
      SysPete)
    
    [ ENHANCEMENTS ]
    * None

    [ DOCUMENTATION ]
    * None

0.300001  2020-04-06 12:14:47-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1461: Relax redirect to match RFC_7231 (James Raspass)
    * GH #1499: Don't double decode date strings (simbabque)
    * GH #1536: Fix storing objects in YAML sessions (Tom Hukins)

    [ ENHANCEMENTS ]
    * GH #1540: Improve configuration handling (Tom Hukins)

    [ DOCUMENTATION ]
    * GH #1425: Revamped Tutorial (Steve Dondley)
    * GH #1521: Documentation on configuring the adress to listen on: 
      "server" does not seem to work, but "host" does (Ma_Sys.ma)
    * GH #1530: Fix missing space in docs (ferki)
    * GH #1535: Fix example code in SessionFactory::File (Sergiy Borodych)
    * GH #1538: Defined or should not be an assignment (Timothy Alexis 
      Vass)

0.300000  2019-12-23 23:55:09-06:00 America/Chicago

    [ BUG FIXES ]
    * None

    [ ENHANCEMENTS ]
    * GH #1127, GH #1476: Route parameters with types (Peter Mottram - 
      SysPete)

    [ DOCUMENTATION ]
    * None 

0.208002  2019-12-14 16:08:46-05:00 America/New_York

    [ BUG FIXES ]
    * GH#1527: Update travis dist to 'trusty' (Sergiy Borodych)

    [ ENHANCEMENTS ]
    * GH #1525: Remove use of Return::MultiLevel, and implement stack frame 
      jumping manually (Graham Knop)

    [ DOCUMENTATION ]
    * GH #1505: Fix Flaskr link (Mohammad S Anwar)
    * GH #1506, 1520: Explain what add_route() does with args (Tom Hukins)
    * GH #1519: Fix Template Toolkit config docs (Tom Hukins)
    * GH #1522: Fix itetare typo (Stefan Hornburg - Racke)
    * GH #1523: Fix typo in Template Toolkit documentation (Mike Katasonov)
    * GH #1524: Fix error in configuration documentation (Tom Hukins)
    * GH #1526: Mention that TT2 config start_tag/end_tag need escaping 
      (Chris White)
    * GH #1528: Note that"Engines" key must be merged in config.yml (Chris
      White)

0.208001  2019-08-04 21:06:25-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1515: Add Types::Standard to cpanfile (Russell @veryrusty Jenkins)

    [ ENHANCEMENTS ]
    * None

    [ DOCUMENTATION ]
    * GH #1513: Fix Dancer2::Test typo (Utkarsh Gupta)

0.208000  2019-06-19 10:21:16-04:00 America/New_York

    [ BUG FIXES ]
    * PR #1493: Fix body not being sent on forward (Johannes Piehler)
    * PR #1498: Load missing Encode in logger role (simbabque)
    * PR #1501: Set :raw when copying files to new project (xenu)
    * GH #1502: Update jquery (racke)

    [ ENHANCEMENTS ]
    * GH #1320: Implement prepare_app keyword (Sawyer X)

    [ DOCUMENTATION ]
    * Tidy up Cookbook POD. (Mohammad S Anwar)

0.207000  2018-11-14 17:24:25-05:00 America/New_York

    [ BUG FIXES ]
    * GH #1427: Allow layout_dir to be configured by set keyword (Russell
      @veryrusty Jenkins)
    * GH #1456: Engine logging respects minimum level filtering (Daniel Perrett)
    * PR #1479: Remove arbitrary Perl 5.10 requirement from tests (Dan Book)
    * PR #1480: Correct dynamic HTTP::XSCookies requirement (Dan Book)
    * PR #1486: Install dzil deps for use by Appveyor (Dan Book)

    [ ENHANCEMENTS ]
    * GH #1418: Send plain text content with send_as() (Steve Dondley)
    * PR #1457: Serializer mutable with custom mapping. Also resolves issues
      #795, #973, and #901 (Russell @veryrusty Jenkins, Yanick Champoux, 
      Daniel Böhmer, Steven Humphrey)
    * PR #1459: Add no default middleware feature. Also resolves #1410 
      (Russell @veryrusty Jenkins)
    * GH #1469: Code of Conduct enhancements (MaxPerl)

    [ DOCUMENTATION ]
    * GH #1166: Add behind_proxy docs to Deployment manual (Nuno Ramos 
      Carvalho)
    * GH #1417: Add "set engines" documentation (Deirdre Moran)
    * PR #1450: Add calculator example (Gabor Szabo)
    * PR #1452: Fix Pod formatting for CPAN (simbabque)
    * PR #1454: Fix typos in docs (Gil Magno)
    * PR #1464: Can't set environment with 'set' keyword (Ben Kaufman)
    * PR #1470: Use session for flash and explain in detail (simbabque)
    * PR #1472: Migration, tutorial, other doc fixes (Jason A. Crome)
    * PR #1473: Show support resources after generating new app (Jason A.
      Crome)
    * PR #1474: Use the correct URL for HAProxy (Jason A. Crome)
    * PR #1475: Add manual section for security concerns (Jason A. Crome)
    * PR #1487: Clarify deprecation of Dancer2::Test (Steve Dondley)

0.206000  2018-04-19 22:09:46-04:00 America/New_York

    [ BUG FIXES ]
    * GH #1090, #1406: Replace HTTP::Body with HTTP::Entity::Parser in
      Dancer2::Core::Request. (Russell @veryrusty Jenkins)
    * GH #1292: Fix multiple attribute definitions within Plugins
      (Nigel Gregoire)
    * GH #1304: Fix the order by which config files are loaded, independently
      of their filename extension (Alberto Simões, Russell @veryrusty Jenkins)
    * GH #1400: Fix infinite recursion with exceptions that use circular
      references. (Andre Walker)
    * GH #1430: Fix `dancer2 gen` from source directory when Dancer2 not
      installed. (Tina @perlpunk Müller - Tina)
    * GH #1434: Add `validate_id` method to verify a session id before
      requesting the session engine fetch it from its data store.
      (Russell @veryrusty Jenkins)
    * GH #1435, #1438: Allow XS crush_cookie methods to return an arrayref
      of values. (Russell @veryrusty Jenkins)
    * GH #1443: Update copyright year (Joseph Frazer)
    * GH #1445: Use latest HTTP::Headers::Fast (Russell @veryrusty Jenkins)
    * PR #1447: Fix missing build requires (Mohammad S Anwar)
    
    [ ENHANCEMENTS ]
    * PR #1354: TemplateToolkit template engine will log (at debug level)
      if a template is not found. (Kiel R Stirling, Russell @veryrusty Jenkins)
    * GH #1432: Support Content-Disposition of inline in 
      send_file() (Dave Webb)
    * PR #1433: Verbose testing in AppVeyor (Graham Knop)

    [ DOCUMENTATION ]
    * GH #1314: Documentation tweaks (David Precious)
    * GH #1317: Document serializer configuration (sdeseille)
    * GH #1386: Add Hello World example (Gabor Szabo)
    * PR #1408: List project development resources (Steve Dondley)
    * PR #1426: Move performance improvement information from Migration guide
      to Deployment (Pedro Melo)

0.206000_02 2018-04-09 21:48:24-04:00 America/New_York (TRIAL RELEASE)

    [ BUG FIXES ]
    * GH #1090, #1406: Replace HTTP::Body with HTTP::Entity::Parser in
      Dancer2::Core::Request. (Russell @veryrusty Jenkins)
    * GH #1304: Fix the order by which config files are loaded, independently
      of their filename extension (Alberto Simões, Russell @veryrusty Jenkins)
    * GH #1400: Fix infinite recursion with exceptions that use circular
      references. (Andre Walker)
    * GH #1430: Fix `dancer2 gen` from source directory when Dancer2 not
      installed. (Tina @perlpunk Müller - Tina)
    * GH #1434: Add `validate_id` method to verify a session id before
      requesting the session engine fetch it from its data store.
      (Russell @veryrusty Jenkins)
    * GH #1435, #1438: Allow XS crush_cookie methods to return an arrayref
      of values. (Russell @veryrusty Jenkins)
    * GH #1443: Update copyright year (Joseph Frazer)
    * GH #1445: Use latest HTTP::Headers::Fast (Russell @veryrusty Jenkins)
    
    [ ENHANCEMENTS ]
    * PR #1354: TemplateToolkit template engine will log (at debug level)
      if a template is not found. (Kiel R Stirling, Russell @veryrusty Jenkins)
    * GH #1432: Support Content-Disposition of inline in 
      send_file() (Dave Webb)
    * PR #1433: Verbose testing in AppVeyor (Graham Knop)

    [ DOCUMENTATION ]
    * GH #1317: Document serializer configuration (sdeseille)
    * PR #1426: Move performance improvement information from Migration guide
      to Deployment (Pedro Melo)

0.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).
t000755000765000024          015154413402 12551 5ustar00jasonstaff000000000000Dancer2-2.1.0app.t100644000765000024      1625015154413402 13702 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 Path::Tiny qw< path >;

# our app/dispatcher object
my $app = Dancer2::Core::App->new( name => 'main', );
$app->setting( show_stacktrace => 1 ); # enable show stacktrace
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 'Check to ensure that add_route can override a prefix, even with undef. (gh-1663)';

my @more_routes = (
    {   method => 'get',
        regexp => '/prefix_test',
        code   => sub {'/prefix_test'},
    },
    {   method => 'get',
        regexp => '/prefix_override_test',
        prefix => '/prefixtest2',
        code   => sub {'/prefix_override_test'},
    },
    {   method => 'get',
        regexp => '/noprefix_test',
        code   => sub {'/noprefix_test'},
   prefix => undef,
    },
);
$app->prefix('/prefixtest');

for my $r (@more_routes) {
   $app->add_route(%$r);
}

my $expected_retvals = {
   '/prefix_test' => 404,
   '/prefixtest/prefix_test' => 200,
   '/prefixtest2/prefix_test' => 404,
   '/prefix_override_test' => 404,
   '/prefixtest/prefix_override_test' => 404,
   '/prefixtest2/prefix_override_test' => 200,
   '/noprefix_test' => 200,
   '/prefixtest/noprefix_test' => 404,
   '/prefixtest2/noprefix_test' => 404,
};

my $expected = {
   '/prefix_test' => undef,
   '/prefixtest/prefix_test' => '/prefix_test',
   '/prefixtest2/prefix_test' => undef,
   '/prefix_override_test' => undef,
   '/prefixtest/prefix_override_test' => undef,
   '/prefixtest2/prefix_override_test' => '/prefix_override_test',
   '/noprefix_test' => '/noprefix_test',
   '/prefixtest/noprefix_test' => undef,
   '/prefixtest2/noprefix_test' => undef,
};

for my $path ( sort keys %$expected_retvals ) {
   my $env = {
       SERVER_PORT => 5000,
       SERVER_NAME => 'test.local',
       REQUEST_METHOD => 'GET',
       PATH_INFO      => $path
   };

   my $resp = $dispatcher->dispatch($env);
   is $resp->[0], $expected_retvals->{$path}, "got expected return value on $path";
   next if $expected_retvals->{$path} == 404;
   is $resp->[2][0], $expected->{$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, these ones...
# set configuration for an engine
$app->config->{'engines'}{'template'}{'Tiny'}{'hello'} = 'world';
$app->config->{'engines'}{'template'}{'Some::Other::Template::Namespace'}{'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_deeply(
    $app->_get_config_for_engine( template => '+Some::Other::Template::Namespace', $app->config ),
    { hello => 'world' },
    '_get_config_for_engine can find the right configuration',
);

is(
    path( $app->caller ),
    path( qw< t app.t > ),
    'Correct caller for app',
);

done_testing;
cpanfile100644000765000024       616215154413402 14157 0ustar00jasonstaff000000000000Dancer2-2.1.0requires 'perl', 5.014000;
requires 'Attribute::Handlers';
requires 'Carp';
requires 'Clone';
requires 'Config::Any';
requires 'Data::Censor' => '0.04';
requires 'Digest::SHA';
requires 'Encode';
requires 'Exporter', '5.57';
requires 'Exporter::Tiny';
requires 'File::Copy';
requires 'File::Path';
requires 'File::Share';
requires 'File::Temp';
requires 'Hash::Merge::Simple';
requires 'Hash::MultiValue';
requires 'HTTP::Date';
requires 'HTTP::Headers::Fast', '0.21';
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 'Path::Tiny';
requires 'Plack', '1.0040';
requires 'Plack::Middleware::FixMissingBodyInRedirect';
requires 'Plack::Middleware::RemoveRedundantBody';
requires 'POSIX';
requires 'Ref::Util';
requires 'Safe::Isa';
requires 'Sub::Quote';
requires 'Template';
requires 'Template::Tiny', '1.16';
requires 'Test::Builder';
requires 'Test::More';
requires 'Types::Standard';
requires 'Type::Tiny', '1.000006';
requires 'URI::Escape';
requires 'CLI::Osprey';
requires 'File::Which';
requires 'Sub::Util';

requires 'Role::Tiny', '2.000000';
conflicts 'Role::Tiny', '== 2.000007';

# Module::Pluggable 6.1 and 6.2 fail their test suite when run as root,
# such as under docker in a github action
requires 'Module::Pluggable';
conflicts 'Module::Pluggable', '== 6.1';
conflicts 'Module::Pluggable', '== 6.2';

# 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.000015';
recommends 'HTTP::XSHeaders';
recommends 'Math::Random::ISAAC::XS';
recommends 'MooX::TypeTiny';
recommends 'Pod::Simple::Search';
recommends 'Pod::Simple::SimpleTree';
recommends 'Type::Tiny::XS';
recommends 'URL::Encode::XS';
recommends 'YAML::XS';
recommends 'Unicode::UTF8';

suggests 'Fcntl';
suggests 'MIME::Types';

test_requires 'Capture::Tiny', '0.12';
test_requires 'HTTP::Cookies';
test_requires 'HTTP::Headers';
test_requires 'Pod::Simple::SimpleTree';
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';
test_requires 'Test::Exception';

author_requires 'Test::NoTabs';
author_requires 'Test::Pod';
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.t100644000765000024       413415154413402 14036 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.t100644000765000024       230115154413402 14021 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.t100644000765000024       112615154413402 14051 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.yml100644000765000024      1733715154413402 13752 0ustar00jasonstaff000000000000Dancer2-2.1.0---
abstract: 'Lightweight yet powerful web application framework'
author:
  - 'Dancer Core Developers'
build_requires:
  Capture::Tiny: '0.12'
  ExtUtils::MakeMaker: '0'
  File::Spec: '0'
  HTTP::Cookies: '0'
  HTTP::Headers: '0'
  IO::Handle: '0'
  IPC::Open3: '0'
  Pod::Simple::SimpleTree: '0'
  Template: '0'
  Test::Builder: '0'
  Test::EOL: '0'
  Test::Exception: '0'
  Test::Fatal: '0'
  Test::More: '0.92'
configure_requires:
  CPAN::Meta::Requirements: '2.120620'
  ExtUtils::MakeMaker: '0'
  File::ShareDir::Install: '0.06'
  Module::Metadata: '0'
conflicts:
  Module::Pluggable: '== 6.2'
  Role::Tiny: '== 2.000007'
  YAML: '== 1.16'
dynamic_config: 1
generated_by: 'Dist::Zilla version 6.037, CPAN::Meta::Converter version 2.150010'
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: v2.1.0
  Dancer2::CLI:
    file: lib/Dancer2/CLI.pm
    version: v2.1.0
  Dancer2::CLI::Gen:
    file: lib/Dancer2/CLI/Gen.pm
    version: v2.1.0
  Dancer2::CLI::Version:
    file: lib/Dancer2/CLI/Version.pm
    version: v2.1.0
  Dancer2::ConfigReader:
    file: lib/Dancer2/ConfigReader.pm
    version: v2.1.0
  Dancer2::ConfigReader::Config::Any:
    file: lib/Dancer2/ConfigReader/Config/Any.pm
    version: v2.1.0
  Dancer2::ConfigUtils:
    file: lib/Dancer2/ConfigUtils.pm
    version: v2.1.0
  Dancer2::Core:
    file: lib/Dancer2/Core.pm
    version: v2.1.0
  Dancer2::Core::App:
    file: lib/Dancer2/Core/App.pm
    version: v2.1.0
  Dancer2::Core::Cookie:
    file: lib/Dancer2/Core/Cookie.pm
    version: v2.1.0
  Dancer2::Core::DSL:
    file: lib/Dancer2/Core/DSL.pm
    version: v2.1.0
  Dancer2::Core::Dispatcher:
    file: lib/Dancer2/Core/Dispatcher.pm
    version: v2.1.0
  Dancer2::Core::Error:
    file: lib/Dancer2/Core/Error.pm
    version: v2.1.0
  Dancer2::Core::Factory:
    file: lib/Dancer2/Core/Factory.pm
    version: v2.1.0
  Dancer2::Core::HTTP:
    file: lib/Dancer2/Core/HTTP.pm
    version: v2.1.0
  Dancer2::Core::Hook:
    file: lib/Dancer2/Core/Hook.pm
    version: v2.1.0
  Dancer2::Core::MIME:
    file: lib/Dancer2/Core/MIME.pm
    version: v2.1.0
  Dancer2::Core::Request:
    file: lib/Dancer2/Core/Request.pm
    version: v2.1.0
  Dancer2::Core::Request::Upload:
    file: lib/Dancer2/Core/Request/Upload.pm
    version: v2.1.0
  Dancer2::Core::Response:
    file: lib/Dancer2/Core/Response.pm
    version: v2.1.0
  Dancer2::Core::Response::Delayed:
    file: lib/Dancer2/Core/Response/Delayed.pm
    version: v2.1.0
  Dancer2::Core::Role::ConfigReader:
    file: lib/Dancer2/Core/Role/ConfigReader.pm
    version: v2.1.0
  Dancer2::Core::Role::DSL:
    file: lib/Dancer2/Core/Role/DSL.pm
    version: v2.1.0
  Dancer2::Core::Role::Engine:
    file: lib/Dancer2/Core/Role/Engine.pm
    version: v2.1.0
  Dancer2::Core::Role::Handler:
    file: lib/Dancer2/Core/Role/Handler.pm
    version: v2.1.0
  Dancer2::Core::Role::HasConfig:
    file: lib/Dancer2/Core/Role/HasConfig.pm
    version: v2.1.0
  Dancer2::Core::Role::HasEnvironment:
    file: lib/Dancer2/Core/Role/HasEnvironment.pm
    version: v2.1.0
  Dancer2::Core::Role::HasLocation:
    file: lib/Dancer2/Core/Role/HasLocation.pm
    version: v2.1.0
  Dancer2::Core::Role::Hookable:
    file: lib/Dancer2/Core/Role/Hookable.pm
    version: v2.1.0
  Dancer2::Core::Role::Logger:
    file: lib/Dancer2/Core/Role/Logger.pm
    version: v2.1.0
  Dancer2::Core::Role::Serializer:
    file: lib/Dancer2/Core/Role/Serializer.pm
    version: v2.1.0
  Dancer2::Core::Role::SessionFactory:
    file: lib/Dancer2/Core/Role/SessionFactory.pm
    version: v2.1.0
  Dancer2::Core::Role::SessionFactory::File:
    file: lib/Dancer2/Core/Role/SessionFactory/File.pm
    version: v2.1.0
  Dancer2::Core::Role::StandardResponses:
    file: lib/Dancer2/Core/Role/StandardResponses.pm
    version: v2.1.0
  Dancer2::Core::Role::Template:
    file: lib/Dancer2/Core/Role/Template.pm
    version: v2.1.0
  Dancer2::Core::Route:
    file: lib/Dancer2/Core/Route.pm
    version: v2.1.0
  Dancer2::Core::Runner:
    file: lib/Dancer2/Core/Runner.pm
    version: v2.1.0
  Dancer2::Core::Session:
    file: lib/Dancer2/Core/Session.pm
    version: v2.1.0
  Dancer2::Core::Time:
    file: lib/Dancer2/Core/Time.pm
    version: v2.1.0
  Dancer2::Core::Types:
    file: lib/Dancer2/Core/Types.pm
    version: v2.1.0
  Dancer2::FileUtils:
    file: lib/Dancer2/FileUtils.pm
    version: v2.1.0
  Dancer2::Handler::AutoPage:
    file: lib/Dancer2/Handler/AutoPage.pm
    version: v2.1.0
  Dancer2::Handler::File:
    file: lib/Dancer2/Handler/File.pm
    version: v2.1.0
  Dancer2::Logger::Capture:
    file: lib/Dancer2/Logger/Capture.pm
    version: v2.1.0
  Dancer2::Logger::Capture::Trap:
    file: lib/Dancer2/Logger/Capture/Trap.pm
    version: v2.1.0
  Dancer2::Logger::Console:
    file: lib/Dancer2/Logger/Console.pm
    version: v2.1.0
  Dancer2::Logger::Diag:
    file: lib/Dancer2/Logger/Diag.pm
    version: v2.1.0
  Dancer2::Logger::File:
    file: lib/Dancer2/Logger/File.pm
    version: v2.1.0
  Dancer2::Logger::Note:
    file: lib/Dancer2/Logger/Note.pm
    version: v2.1.0
  Dancer2::Logger::Null:
    file: lib/Dancer2/Logger/Null.pm
    version: v2.1.0
  Dancer2::Plugin:
    file: lib/Dancer2/Plugin.pm
    version: v2.1.0
  Dancer2::Serializer::Dumper:
    file: lib/Dancer2/Serializer/Dumper.pm
    version: v2.1.0
  Dancer2::Serializer::JSON:
    file: lib/Dancer2/Serializer/JSON.pm
    version: v2.1.0
  Dancer2::Serializer::Mutable:
    file: lib/Dancer2/Serializer/Mutable.pm
    version: v2.1.0
  Dancer2::Serializer::YAML:
    file: lib/Dancer2/Serializer/YAML.pm
    version: v2.1.0
  Dancer2::Session::Simple:
    file: lib/Dancer2/Session/Simple.pm
    version: v2.1.0
  Dancer2::Session::YAML:
    file: lib/Dancer2/Session/YAML.pm
    version: v2.1.0
  Dancer2::Template::TemplateToolkit:
    file: lib/Dancer2/Template/TemplateToolkit.pm
    version: v2.1.0
  Dancer2::Template::Tiny:
    file: lib/Dancer2/Template/Tiny.pm
    version: v2.1.0
  Dancer2::Test:
    file: lib/Dancer2/Test.pm
    version: v2.1.0
recommends:
  CGI::Deurl::XS: '0'
  Class::XSAccessor: '0'
  Cpanel::JSON::XS: '0'
  Crypt::URandom: '0'
  HTTP::XSCookies: '0.000015'
  HTTP::XSHeaders: '0'
  Math::Random::ISAAC::XS: '0'
  MooX::TypeTiny: '0'
  Pod::Simple::Search: '0'
  Pod::Simple::SimpleTree: '0'
  Type::Tiny::XS: '0'
  URL::Encode::XS: '0'
  Unicode::UTF8: '0'
  YAML::XS: '0'
requires:
  Attribute::Handlers: '0'
  CLI::Osprey: '0'
  Carp: '0'
  Clone: '0'
  Config::Any: '0'
  Data::Censor: '0.04'
  Digest::SHA: '0'
  Encode: '0'
  Exporter: '5.57'
  Exporter::Tiny: '0'
  File::Copy: '0'
  File::Path: '0'
  File::Share: '0'
  File::Temp: '0'
  File::Which: '0'
  HTTP::Date: '0'
  HTTP::Headers::Fast: '0.21'
  HTTP::Tiny: '0'
  Hash::Merge::Simple: '0'
  Hash::MultiValue: '0'
  Import::Into: '0'
  JSON::MaybeXS: '0'
  List::Util: '1.29'
  MIME::Base64: '3.13'
  Module::Pluggable: '0'
  Module::Runtime: '0'
  Moo: '2.000000'
  Moo::Role: '0'
  POSIX: '0'
  Path::Tiny: '0'
  Plack: '1.0040'
  Plack::Middleware::FixMissingBodyInRedirect: '0'
  Plack::Middleware::RemoveRedundantBody: '0'
  Ref::Util: '0'
  Role::Tiny: '2.000000'
  Safe::Isa: '0'
  Sub::Quote: '0'
  Sub::Util: '0'
  Template: '0'
  Template::Tiny: '1.16'
  Test::Builder: '0'
  Test::More: '0.92'
  Type::Tiny: '1.000006'
  Types::Standard: '0'
  URI::Escape: '0'
  YAML: '0.86'
  parent: '0'
  perl: '5.014'
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: 2.1.0
x_generated_by_perl: v5.42.0
x_serialization_backend: 'YAML::Tiny version 1.76'
x_spdx_expression: 'Artistic-1.0-Perl OR GPL-1.0-or-later'
MANIFEST100644000765000024      2767315154413402 13636 0ustar00jasonstaff000000000000Dancer2-2.1.0# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.037
AUTHORS
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Changes
LICENSE
MANIFEST
META.json
META.yml
Makefile.PL
README.md
Releasing-Dancer2.md
SECURITY.md
cpanfile
examples/single/hello_world.psgi
examples/single/simple_calculator.psgi
lib/Dancer2.pm
lib/Dancer2/CLI.pm
lib/Dancer2/CLI/Gen.pm
lib/Dancer2/CLI/Version.pm
lib/Dancer2/ConfigReader.pm
lib/Dancer2/ConfigReader/Config/Any.pm
lib/Dancer2/ConfigUtils.pm
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/HasConfig.pm
lib/Dancer2/Core/Role/HasEnvironment.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/DeprecationPolicy.pod
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/Config.pod
lib/Dancer2/Manual/Cookbook.pod
lib/Dancer2/Manual/Deployment.pod
lib/Dancer2/Manual/Extending.pod
lib/Dancer2/Manual/Keywords.pod
lib/Dancer2/Manual/Migration.pod
lib/Dancer2/Manual/Plugins.pod
lib/Dancer2/Manual/QuickStart.pod
lib/Dancer2/Manual/Testing.pod
lib/Dancer2/Manual/Tutorial.pod
lib/Dancer2/Plugin.pm
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/TemplateToolkit.pm
lib/Dancer2/Template/Tiny.pm
lib/Dancer2/Test.pm
script/dancer2
share/.gitignore
share/docker/Dockerfile
share/skel/default/.dancer
share/skel/default/MANIFEST.SKIP
share/skel/default/Makefile.PL
share/skel/default/bin/+app.psgi
share/skel/default/config.yml
share/skel/default/cpanfile
share/skel/default/environments/development.yml
share/skel/default/environments/production.yml
share/skel/default/lib/AppFile.pm
share/skel/default/public/+dispatch.cgi
share/skel/default/public/+dispatch.fcgi
share/skel/default/public/404.html
share/skel/default/public/500.html
share/skel/default/public/css/error.css
share/skel/default/public/css/style.css
share/skel/default/public/favicon.ico
share/skel/default/public/images/perldancer-bg.jpg
share/skel/default/public/images/perldancer.jpg
share/skel/default/t/001_base.t
share/skel/default/t/002_index_route.t
share/skel/default/views/index.tt
share/skel/default/views/layouts/main.tt
share/skel/tutorial/bin/+app.psgi
share/skel/tutorial/config.yml
share/skel/tutorial/cpanfile
share/skel/tutorial/db/blog.db
share/skel/tutorial/db/entries.sql
share/skel/tutorial/db/users.sql
share/skel/tutorial/environments/development.yml
share/skel/tutorial/environments/production.yml
share/skel/tutorial/environments/test.yml
share/skel/tutorial/lib/AppFile.pm
share/skel/tutorial/lib/AppFile/Schema.pm
share/skel/tutorial/lib/AppFile/Schema/Result/Entry.pm
share/skel/tutorial/lib/AppFile/Schema/Result/User.pm
share/skel/tutorial/public/404.html
share/skel/tutorial/public/500.html
share/skel/tutorial/public/css/error.css
share/skel/tutorial/public/css/style.css
share/skel/tutorial/public/dispatch.cgi
share/skel/tutorial/public/dispatch.fcgi
share/skel/tutorial/public/favicon.ico
share/skel/tutorial/t/001_base.t
share/skel/tutorial/t/002_index_route.t
share/skel/tutorial/t/003_login.t
share/skel/tutorial/t/004_blog.t
share/skel/tutorial/t/db/test.db
share/skel/tutorial/views/create_update.tt
share/skel/tutorial/views/delete.tt
share/skel/tutorial/views/entry.tt
share/skel/tutorial/views/index.tt
share/skel/tutorial/views/layouts/main.tt
share/skel/tutorial/views/login.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/t3/lib/App4.pm
t/app/t3/lib/config.yml
t/app/t_config_file_extended/bin/app.psgi
t/app/t_config_file_extended/config.yml
t/app/t_config_file_extended/lib/App1.pm
t/app_alone.t
t/author-distmeta.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-HasConfig/with.t
t/classes/Dancer2-Core-Role-HasEnvironment/with.t
t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists
t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/blib/lib/fakescript.pl
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.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_file_extended.t
t/config_many.t
t/config_many_failure.t
t/config_multiapp.t
t/config_reader.t
t/config_settings.t
t/config_utils.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/empty.foo
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/mime.t
t/dsl/parameters.t
t/dsl/pass.t
t/dsl/path.t
t/dsl/pod.t
t/dsl/request.t
t/dsl/request_data.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/dsl/uri_for.t
t/dsl/uri_for_route.t
t/dsl/yaml.t
t/engine.t
t/error.t
t/examples/hello_world.t
t/examples/simple_calculator.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-1289.t
t/issues/gh-1449/TestPlugin.pm
t/issues/gh-1449/gh-1449.t
t/issues/gh-1564.t
t/issues/gh-1621/gh-1621.t
t/issues/gh-1621/views/redirect.tt
t/issues/gh-1664/gh-1664.t
t/issues/gh-1712/gh-1712.t
t/issues/gh-1712/views/exec.tt
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/ConfigReader/Additional.pm
t/lib/Dancer2/ConfigReader/File/Extended.pm
t/lib/Dancer2/ConfigReader/Recursive.pm
t/lib/Dancer2/ConfigReader/TestDummy.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/Dancer2/Template/TemplateToolkitFoo.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/TemplateFoo.pm
t/lib/TestApp.pm
t/lib/TestPod.pm
t/lib/TestTypeLibrary.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/multipart_content.t
t/named_apps.t
t/named_routes.t
t/no_default_middleware.t
t/plugin2/app_dsl_cb/app_dsl_cb.t
t/plugin2/app_dsl_cb/lib/App.pm
t/plugin2/app_dsl_cb/lib/App/TestPlugin.pm
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_multipart_formdata.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/serializer_mutable_custom.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/session_yaml_object.t
t/shared_engines.t
t/static_content.t
t/strict_config.t
t/template.t
t/template_default_tokens.t
t/template_ext.t
t/template_name.t
t/time.t
t/types.t
t/utf8_strict.t
t/utf8_url.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
README.md100644000765000024      2026215154413402 13747 0ustar00jasonstaff000000000000Dancer2-2.1.0

Perl Dancer logo

Dancer2 is a lightweight yet powerful web application framework written in Perl.
Tutorial · Manual · Discussion Forums · Public Wiki · Mailing List


Dancer2 is the evolution of [Dancer](https://metacpan.org/pod/Dancer) and is based on on [Moo](https://metacpan.org/pod/Moo), a lightweight object framework for Perl. Dancer2 can optionally use XS modules for speed, but at its core remains fatpackable (via [App::FatPacker](https://metacpan.org/pod/App%3A%3AFatPacker)), allowing you to easily deploy Dancer2 applications in environments that do not support custom installations of CPAN modules. Dancer2 is easy and fun: use Dancer2; get '/' => sub { "Hello World" }; dance; ## Documentation Index You have questions. We have answers. - Dancer2 Tutorial Want to learn by example? The [Dancer2 tutorial](https://metacpan.org/pod/Dancer2%3A%3AManual%3A%3ATutorial) will take you from installation to a working application. - Quick Start Want to get going faster? [Quick Start](https://metacpan.org/dist/Dancer2/view/lib/Dancer2/Manual/QuickStart.pod) will help you install Dancer2 and bootstrap a new application quickly. - Manual Want to gain understanding of Dancer2 so you can use it best? The [Dancer2::Manual](https://metacpan.org/pod/Dancer2%3A%3AManual) is a comprehensive guide to the framework. - Keyword Guide Looking for a list of all the keywords? The [DSL guide](https://metacpan.org/pod/Dancer2%3A%3AManual%3A%3AKeywords) documents the entire Dancer2 DSL. - Configuration Need to fine tune your application? The [configuration guide](https://metacpan.org/pod/Dancer2%3A%3AConfig) is a complete reference to all configuration options. - Deployment Ready to get your application off the ground? [Deploying Dancer2 Applications](https://metacpan.org/pod/Dancer2%3A%3AManual%3A%3ADeployment) helps you deploy your application to a real-world host. - Cookbook How do I...? Our [cookbook](https://metacpan.org/dist/Dancer2/view/lib/Dancer2/Manual/Cookbook.pod) comes with various recipes in many tasty flavors! - Plugins Looking for add-on functionality for your application? The [plugin guide](https://metacpan.org/pod/Dancer2%3A%3APlugins) contains our curated list of recommended plugins. For information on how to author a plugin, see [the plugin author's guide](https://metacpan.org/pod/Dancer2%3A%3APlugin#Writing-the-plugin). - Dancer2 Migration Guide Starting from Dancer 1? Jump over to [the migration guide](https://metacpan.org/pod/Dancer2%3A%3AManual%3A%3AMigration) to learn how to make the smoothest transition to Dancer2. ### Other Documentation - Core and Community Policy, and Standards of Conduct The [Dancer core and community policy, and standards of conduct](https://metacpan.org/pod/Dancer2%3A%3APolicy) defines what constitutes acceptable behavior in our community, what behavior is considered abusive and unacceptable, and what steps will be taken to remediate inappropriate and abusive behavior. By participating in any public forum for Dancer or its community, you are agreeing to the terms of this policy. - GitHub Wiki Our [wiki](https://github.com/PerlDancer/Dancer2/wiki) has community-contributed documentation, as well as other information that doesn't quite fit within this manual. - Contributing The [contribution guidelines](https://github.com/PerlDancer/Dancer2/blob/main/CONTRIBUTING.md) describe how to set up your development environment to contribute to the development of Dancer2, Dancer2's Git workflow, submission guidelines, and various coding standards. - Deprecation Policy The [deprecation policy](https://metacpan.org/pod/Dancer2%3A%3ADeprecationPolicy) defines the process for removing old, broken, unused, or outdated code from the Dancer2 codebase. This policy is critical for guiding and shaping future development of Dancer2. # Security Reports If you need to report a security vulnerability in Dancer2, send all pertinent information to [dancer-security@dancer.pm](mailto:dancer-security@dancer.pm), or report it via the GitHub security tool. These reports will be addressed in the earliest possible timeframe. # Support You are welcome to join our mailing list. For subscription information, mail address and archives see [http://lists.preshweb.co.uk/mailman/listinfo/dancer-users](http://lists.preshweb.co.uk/mailman/listinfo/dancer-users). We are also on IRC: #dancer on irc.perl.org. # Authors ## Dancer Core Team Alberto Simões Alexis Sukrieh D Ruth Holloway (GeekRuthie) Damien Krotkine David Precious Franck Cuny Jason A. Crome Mickey Nasriachi Peter Mottram (SysPete) Russell Jenkins Sawyer X Stefan Hornburg (Racke) Yanick Champoux ## Core Team Emeritus David Golden Steven Humphrey ## Contributors A. Sinan Unur Abdullah Diab Achyut Kumar Panda Ahmad M. Zawawi Alex Beamish Alexander Karelas Alexander Pankoff Alexandr Ciornii Andrew Beverley Andrew Grangaard Andrew Inishev Andrew Solomon Andy Jack Ashvini V B10m Bas Bloemsaat baynes Ben Hutton Ben Kaufman biafra Blabos de Blebe Breno G. de Oliveira cdmalon Celogeek Cesare Gargano Charlie Gonzalez chenchen000 Chi Trinh Christian Walde Christopher White cloveistaken Colin Kuskie cym0n Dale Gallagher Dan Book (Grinnz) Daniel Böhmer Daniel Muey Daniel Perrett Dave Jacoby Dave Webb David (sbts) David Steinbrunner David Zurborg Davs Deirdre Moran Dennis Lichtenthäler Dinis Rebolo dtcyganov Elliot Holden Emil Perhinschi Erik Smit Fayland Lam ferki Gabor Szabo GeekRuthie geistteufel Gideon D'souza Gil Magno Glenn Fowler Graham Knop Gregor Herrmann Grzegorz Rożniecki Hobbestigrou Hunter McMillen ice-lenor Ivan Bessarabov Ivan Kruglov JaHIY Jakob Voss James Aitken James Raspass James McCoy Jason Lewis Javier Rojas Jean Stebens Jens Rehsack Joel Berger Johannes Piehler Jonathan Cast Jonathan Scott Duff Joseph Frazer Julien Fiegehenn (simbabque) 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 mauke Maurice MaxPerl Ma_Sys.ma Menno Blom Michael Kröll Michał Wojciechowski Mike Katasonov Mohammad S Anwar mokko Nick Patch Nick Tonkin Nigel Gregoire Nikita K Nuno Carvalho Olaf Alders Olivier Mengué Omar M. Othman pants Patrick Zimmermann Pau Amma Paul Clements Paul Cochrane Paul Williams Pedro Bruno Pedro Melo Philippe Bricout Ricardo Signes Rick Yakubowski Ruben Amortegui Sakshee Vijay (sakshee3) Sam Kington Samit Badle Sebastien Deseille (sdeseille) Sergiy Borodych Shlomi Fish Slava Goltser Snigdha Steve Bertrand Steve Dondley Steven Humphrey Tatsuhiko Miyagawa Timothy Alexis Vass Tina Müller Tom Hukins Upasana Shukla Utkarsh Gupta Vernon Lyon Victor Adam Vince Willems Vincent Bachelier xenu Yves Orton # Author Dancer Core Developers # Copyright and License This software is copyright (c) 2024 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. error.t100644000765000024 2216415154413402 14254 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use lib 't/lib'; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; use List::Util qw; use Module::Runtime qw/ require_module /; 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_stacktrace => 1 )->throw($resp) }; isa_ok($err, 'Dancer2::Core::Response', "Error->throw() accepts a response"); }; subtest 'Error with show_stacktrace: 0' => sub { my $err = Dancer2::Core::Error->new( exception => 'our exception', show_stacktrace => 0 )->throw; unlike $err->content => qr/our exception/; }; subtest 'Error with show_stacktrace: 1' => sub { my $err = Dancer2::Core::Error->new( exception => 'our exception', show_stacktrace => 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 $exception = $@; my $err = Dancer2::Core::Error->new( app => Dancer2::Core::App->new(), exception => $exception, show_stacktrace => 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" ); }; subtest 'Errors with show_stacktrace and circular references' => sub { { package App::ShowErrorsCircRef; use Dancer2; set show_stacktrace => 1; set something_with_config => {something => config}; set password => '===VERY-UNIQUE-STRING==='; set innocent_thing => '===VERY-INNOCENT-STRING==='; set template => 'tiny'; # Trigger an error that makes Dancer2::Core::Error::_censor enter an # infinite loop get '/ohno' => sub { template q{I don't exist}; }; } my $test = Plack::Test->create( App::ShowErrorsCircRef->to_app ); my $r = $test->request( GET '/ohno' ); is( $r->code, 500, "/ohno returned 500 response"); like( $r->content, qr{Stack}, 'it includes a stack trace' ); my @password_values = ($r->content =~ /\bpassword\b(.+)\n/g); my $is_password_hidden = all { /Hidden \(looks potentially sensitive\)/ } @password_values; ok($is_password_hidden, "password was hidden in stacktrace"); cmp_ok(@password_values, '>', 1, 'password key appears more than once in the stacktrace'); unlike($r->content, qr{===VERY-UNIQUE-STRING===}, 'password value does not appear in the stacktrace'); like($r->content, qr{===VERY-INNOCENT-STRING===}, 'Values for other keys (non-sensitive) appear in the stacktrace'); }; subtest censor => sub { sub MyApp::Censor::censor { $_[0]->{hush} = 'NOT TELLING'; return 1; } my $app = Dancer2::Core::App->new( name => 'main' ); $app->setting( password => 'potato' ); # oh my, we're leaking a password subtest 'core censor()' => sub { my $error = Dancer2::Core::Error->new( app => $app ); unlike $error->environment => qr/potato/, 'the password is censored'; like $error->environment => qr/^.*password.*Hidden.*$/m, 'we say it is hidden'; }; subtest 'custom censor' => sub { subtest 'via function string' => sub { my $app = Dancer2::Core::App->new( name => 'main' ); my $error = Dancer2::Core::Error->new( app => $app ); $app->setting( hush => 'potato' ); $app->setting( error_censor => 'MyApp::Censor::censor' ); unlike $error->environment => qr/potato/, 'the password is censored'; like $error->environment => qr/^ .* hush .* NOT \s TELLING .* $/xm, 'we say it is hidden'; }; subtest 'via class hashref' => sub { my $app = Dancer2::Core::App->new( name => 'main' ); $app->setting( 'error_censor' => { 'Data::Censor' => { sensitive_fields => ['hush'], replacement => 'NOT TELLING', } }); my $error = Dancer2::Core::Error->new( app => $app ); $app->setting( hush => 'potato' ); unlike $error->environment => qr/potato/, 'the password is censored'; like $error->environment => qr/^ .* hush .* NOT \s TELLING .* $/xm, 'we say it is hidden'; }; } }; 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} } } hooks.t100644000765000024 2256615154413402 14254 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use Path::Tiny (); 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( Path::Tiny::path(__FILE__)->absolute->stringify, 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/; }; } # 3 test apps for the exception hook - see comments below { package App::HookException; use Dancer2; get '/hook_exception' => sub { return 1 }; hook after => sub { die 'this is an exception in the after hook'; }; hook on_hook_exception => sub { my ($app, $error, $hook_name) = @_; ::is ref($app), 'Dancer2::Core::App'; ::like $error, qr/this is an exception in the after hook/; ::is $hook_name, 'core.app.after_request'; $app->response->content("Setting response from exception hook"); $app->response->halt; }; } { package App::HookExceptionDouble; use Dancer2; get '/hook_exception_double' => sub { return 1 }; hook after => sub { die 'this is an exception in the after hook'; }; hook on_hook_exception => sub { my ($app, $error) = @_; $app->response->content("Setting response from exception hook and then dying"); $app->response->halt; die 'this is an exception in the exception hook'; }; } { package App::HookExceptionRecursive; use Dancer2; get '/hook_exception_recursive' => sub { return 1 }; hook after => sub { die 'this is an exception in the after hook'; }; hook on_hook_exception => sub { my ($app, $error) = @_; die 'this is an exception in the hook exception causing recursion'; }; } 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' ) }; }; # A basic test for a hook that is called in the event of an exception in a hook subtest 'hook_exception' => sub { my $test = Plack::Test->create( App::HookException->to_app ); my $resp = $test->request( GET '/hook_exception' ); is( $resp->content, 'Setting response from exception hook' ); }; # A more advanced test that checks that an exception can take place within the # exception hook, but only if it sets the response content and performs a halt subtest 'hook_exception_double' => sub { my $test = Plack::Test->create( App::HookExceptionDouble->to_app ); my $resp = $test->request( GET '/hook_exception_double'); is( $resp->content, 'Setting response from exception hook and then dying' ); }; # Similar to the above, but without any handling for the second exception which # would otherwise result in a recursive situation subtest 'hook_exception_recursive' => sub { my $test = Plack::Test->create( App::HookExceptionRecursive->to_app ); my $resp = $test->request( GET '/hook_exception_recursive'); like( $resp->content, qr/Internal Server Error/ ); }; subtest 'hook entries logging' => sub { $::hook_counter = 0; package App::HookEntries { use Sub::Util qw/ set_subname /; use Dancer2; set log => 'core'; set logger => 'capture'; $::trap = engine('logger')->trapper; get '/' => sub { 'hello there' }; hook 'before_request' => set_subname my_before => sub { $::hook_counter++; }; sub my_after { $::hook_counter++ } hook 'after_request' => \&my_after; } my $app = App::HookEntries->to_app; my $test = Plack::Test->create( $app ); $test->request( GET '/' ); is $::hook_counter => 2, "we hit both hooks"; my @logs = map {$_->{message}} @{$::trap->read}; for my $hook ( qw/ my_before my_after / ) { ok scalar( grep { /$hook/ } @logs ), "App::HookEntries::$hook" } }; done_testing; types.t100644000765000024 1303015154413402 14257 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' ); META.json100644000765000024 3072115154413402 14112 0ustar00jasonstaff000000000000Dancer2-2.1.0{ "abstract" : "Lightweight yet powerful web application framework", "author" : [ "Dancer Core Developers" ], "dynamic_config" : 1, "generated_by" : "Dist::Zilla version 6.037, CPAN::Meta::Converter version 2.150010", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Dancer2", "prereqs" : { "configure" : { "requires" : { "CPAN::Meta::Requirements" : "2.120620", "ExtUtils::MakeMaker" : "0", "File::ShareDir::Install" : "0.06", "Module::Metadata" : "0" } }, "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" : { "Module::Pluggable" : "== 6.2", "Role::Tiny" : "== 2.000007", "YAML" : "== 1.16" }, "recommends" : { "CGI::Deurl::XS" : "0", "Class::XSAccessor" : "0", "Cpanel::JSON::XS" : "0", "Crypt::URandom" : "0", "HTTP::XSCookies" : "0.000015", "HTTP::XSHeaders" : "0", "Math::Random::ISAAC::XS" : "0", "MooX::TypeTiny" : "0", "Pod::Simple::Search" : "0", "Pod::Simple::SimpleTree" : "0", "Type::Tiny::XS" : "0", "URL::Encode::XS" : "0", "Unicode::UTF8" : "0", "YAML::XS" : "0" }, "requires" : { "Attribute::Handlers" : "0", "CLI::Osprey" : "0", "Carp" : "0", "Clone" : "0", "Config::Any" : "0", "Data::Censor" : "0.04", "Digest::SHA" : "0", "Encode" : "0", "Exporter" : "5.57", "Exporter::Tiny" : "0", "File::Copy" : "0", "File::Path" : "0", "File::Share" : "0", "File::Temp" : "0", "File::Which" : "0", "HTTP::Date" : "0", "HTTP::Headers::Fast" : "0.21", "HTTP::Tiny" : "0", "Hash::Merge::Simple" : "0", "Hash::MultiValue" : "0", "Import::Into" : "0", "JSON::MaybeXS" : "0", "List::Util" : "1.29", "MIME::Base64" : "3.13", "Module::Pluggable" : "0", "Module::Runtime" : "0", "Moo" : "2.000000", "Moo::Role" : "0", "POSIX" : "0", "Path::Tiny" : "0", "Plack" : "1.0040", "Plack::Middleware::FixMissingBodyInRedirect" : "0", "Plack::Middleware::RemoveRedundantBody" : "0", "Ref::Util" : "0", "Role::Tiny" : "2.000000", "Safe::Isa" : "0", "Sub::Quote" : "0", "Sub::Util" : "0", "Template" : "0", "Template::Tiny" : "1.16", "Test::Builder" : "0", "Test::More" : "0.92", "Type::Tiny" : "1.000006", "Types::Standard" : "0", "URI::Escape" : "0", "YAML" : "0.86", "parent" : "0", "perl" : "5.014" }, "suggests" : { "Fcntl" : "0", "MIME::Types" : "0" } }, "test" : { "recommends" : { "CPAN::Meta" : "2.120900" }, "requires" : { "Capture::Tiny" : "0.12", "ExtUtils::MakeMaker" : "0", "File::Spec" : "0", "HTTP::Cookies" : "0", "HTTP::Headers" : "0", "IO::Handle" : "0", "IPC::Open3" : "0", "Pod::Simple::SimpleTree" : "0", "Template" : "0", "Test::Builder" : "0", "Test::EOL" : "0", "Test::Exception" : "0", "Test::Fatal" : "0", "Test::More" : "0.92" } } }, "provides" : { "Dancer2" : { "file" : "lib/Dancer2.pm", "version" : "v2.1.0" }, "Dancer2::CLI" : { "file" : "lib/Dancer2/CLI.pm", "version" : "v2.1.0" }, "Dancer2::CLI::Gen" : { "file" : "lib/Dancer2/CLI/Gen.pm", "version" : "v2.1.0" }, "Dancer2::CLI::Version" : { "file" : "lib/Dancer2/CLI/Version.pm", "version" : "v2.1.0" }, "Dancer2::ConfigReader" : { "file" : "lib/Dancer2/ConfigReader.pm", "version" : "v2.1.0" }, "Dancer2::ConfigReader::Config::Any" : { "file" : "lib/Dancer2/ConfigReader/Config/Any.pm", "version" : "v2.1.0" }, "Dancer2::ConfigUtils" : { "file" : "lib/Dancer2/ConfigUtils.pm", "version" : "v2.1.0" }, "Dancer2::Core" : { "file" : "lib/Dancer2/Core.pm", "version" : "v2.1.0" }, "Dancer2::Core::App" : { "file" : "lib/Dancer2/Core/App.pm", "version" : "v2.1.0" }, "Dancer2::Core::Cookie" : { "file" : "lib/Dancer2/Core/Cookie.pm", "version" : "v2.1.0" }, "Dancer2::Core::DSL" : { "file" : "lib/Dancer2/Core/DSL.pm", "version" : "v2.1.0" }, "Dancer2::Core::Dispatcher" : { "file" : "lib/Dancer2/Core/Dispatcher.pm", "version" : "v2.1.0" }, "Dancer2::Core::Error" : { "file" : "lib/Dancer2/Core/Error.pm", "version" : "v2.1.0" }, "Dancer2::Core::Factory" : { "file" : "lib/Dancer2/Core/Factory.pm", "version" : "v2.1.0" }, "Dancer2::Core::HTTP" : { "file" : "lib/Dancer2/Core/HTTP.pm", "version" : "v2.1.0" }, "Dancer2::Core::Hook" : { "file" : "lib/Dancer2/Core/Hook.pm", "version" : "v2.1.0" }, "Dancer2::Core::MIME" : { "file" : "lib/Dancer2/Core/MIME.pm", "version" : "v2.1.0" }, "Dancer2::Core::Request" : { "file" : "lib/Dancer2/Core/Request.pm", "version" : "v2.1.0" }, "Dancer2::Core::Request::Upload" : { "file" : "lib/Dancer2/Core/Request/Upload.pm", "version" : "v2.1.0" }, "Dancer2::Core::Response" : { "file" : "lib/Dancer2/Core/Response.pm", "version" : "v2.1.0" }, "Dancer2::Core::Response::Delayed" : { "file" : "lib/Dancer2/Core/Response/Delayed.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::ConfigReader" : { "file" : "lib/Dancer2/Core/Role/ConfigReader.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::DSL" : { "file" : "lib/Dancer2/Core/Role/DSL.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::Engine" : { "file" : "lib/Dancer2/Core/Role/Engine.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::Handler" : { "file" : "lib/Dancer2/Core/Role/Handler.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::HasConfig" : { "file" : "lib/Dancer2/Core/Role/HasConfig.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::HasEnvironment" : { "file" : "lib/Dancer2/Core/Role/HasEnvironment.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::HasLocation" : { "file" : "lib/Dancer2/Core/Role/HasLocation.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::Hookable" : { "file" : "lib/Dancer2/Core/Role/Hookable.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::Logger" : { "file" : "lib/Dancer2/Core/Role/Logger.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::Serializer" : { "file" : "lib/Dancer2/Core/Role/Serializer.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::SessionFactory" : { "file" : "lib/Dancer2/Core/Role/SessionFactory.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::SessionFactory::File" : { "file" : "lib/Dancer2/Core/Role/SessionFactory/File.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::StandardResponses" : { "file" : "lib/Dancer2/Core/Role/StandardResponses.pm", "version" : "v2.1.0" }, "Dancer2::Core::Role::Template" : { "file" : "lib/Dancer2/Core/Role/Template.pm", "version" : "v2.1.0" }, "Dancer2::Core::Route" : { "file" : "lib/Dancer2/Core/Route.pm", "version" : "v2.1.0" }, "Dancer2::Core::Runner" : { "file" : "lib/Dancer2/Core/Runner.pm", "version" : "v2.1.0" }, "Dancer2::Core::Session" : { "file" : "lib/Dancer2/Core/Session.pm", "version" : "v2.1.0" }, "Dancer2::Core::Time" : { "file" : "lib/Dancer2/Core/Time.pm", "version" : "v2.1.0" }, "Dancer2::Core::Types" : { "file" : "lib/Dancer2/Core/Types.pm", "version" : "v2.1.0" }, "Dancer2::FileUtils" : { "file" : "lib/Dancer2/FileUtils.pm", "version" : "v2.1.0" }, "Dancer2::Handler::AutoPage" : { "file" : "lib/Dancer2/Handler/AutoPage.pm", "version" : "v2.1.0" }, "Dancer2::Handler::File" : { "file" : "lib/Dancer2/Handler/File.pm", "version" : "v2.1.0" }, "Dancer2::Logger::Capture" : { "file" : "lib/Dancer2/Logger/Capture.pm", "version" : "v2.1.0" }, "Dancer2::Logger::Capture::Trap" : { "file" : "lib/Dancer2/Logger/Capture/Trap.pm", "version" : "v2.1.0" }, "Dancer2::Logger::Console" : { "file" : "lib/Dancer2/Logger/Console.pm", "version" : "v2.1.0" }, "Dancer2::Logger::Diag" : { "file" : "lib/Dancer2/Logger/Diag.pm", "version" : "v2.1.0" }, "Dancer2::Logger::File" : { "file" : "lib/Dancer2/Logger/File.pm", "version" : "v2.1.0" }, "Dancer2::Logger::Note" : { "file" : "lib/Dancer2/Logger/Note.pm", "version" : "v2.1.0" }, "Dancer2::Logger::Null" : { "file" : "lib/Dancer2/Logger/Null.pm", "version" : "v2.1.0" }, "Dancer2::Plugin" : { "file" : "lib/Dancer2/Plugin.pm", "version" : "v2.1.0" }, "Dancer2::Serializer::Dumper" : { "file" : "lib/Dancer2/Serializer/Dumper.pm", "version" : "v2.1.0" }, "Dancer2::Serializer::JSON" : { "file" : "lib/Dancer2/Serializer/JSON.pm", "version" : "v2.1.0" }, "Dancer2::Serializer::Mutable" : { "file" : "lib/Dancer2/Serializer/Mutable.pm", "version" : "v2.1.0" }, "Dancer2::Serializer::YAML" : { "file" : "lib/Dancer2/Serializer/YAML.pm", "version" : "v2.1.0" }, "Dancer2::Session::Simple" : { "file" : "lib/Dancer2/Session/Simple.pm", "version" : "v2.1.0" }, "Dancer2::Session::YAML" : { "file" : "lib/Dancer2/Session/YAML.pm", "version" : "v2.1.0" }, "Dancer2::Template::TemplateToolkit" : { "file" : "lib/Dancer2/Template/TemplateToolkit.pm", "version" : "v2.1.0" }, "Dancer2::Template::Tiny" : { "file" : "lib/Dancer2/Template/Tiny.pm", "version" : "v2.1.0" }, "Dancer2::Test" : { "file" : "lib/Dancer2/Test.pm", "version" : "v2.1.0" } }, "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" : "2.1.0", "x_generated_by_perl" : "v5.42.0", "x_serialization_backend" : "Cpanel::JSON::XS version 4.40", "x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later" } engine.t100644000765000024 263615154413402 14352 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; caller.t100644000765000024 76415154413402 14327 0ustar00jasonstaff000000000000Dancer2-2.1.0/t#!perl use strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; use Path::Tiny qw< path >; { 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( path( $res->content )->stringify, path(qw)->stringify, 'Correct App name from caller', ); }; config.t100644000765000024 471315154413402 14350 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Carp 'croak'; # use Dancer2::Core::Runner; use Path::Tiny qw< path >; use Dancer2::ConfigReader; # 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 = path( __FILE__ )->parent->child('config')->stringify; my $location2 = path( __FILE__ )->parent->child('config2')->stringify; { my $cfgr = Dancer2::ConfigReader->new( environment => 'my_env', location => $location, default_config => { content_type => 'text/html', charset => 'UTF-8', }, ); is( $cfgr->config->{'application'}->{'some_feature'}, 'foo', 'Ok config' ); is( $cfgr->config->{'charset'}, 'utf-8', 'Ok default config' ); } { # note "bad YAML file: environments/failure.yml"; like( exception { Dancer2::ConfigReader->new( environment => 'failure', location => $location, default_config => { }, )->config; }, qr{Unable to parse the configuration file}, 'Configuration file parsing failure', ); } { my $cfgr = Dancer2::ConfigReader->new( environment => 'any_env', location => $location, default_config => { }, ); my $cfg = $cfgr->config; isnt( $cfg, undef, 'OK config read' ); } { my $cfgr = Dancer2::ConfigReader->new( environment => 'merging', location => $location, default_config => { }, ); # note "config merging"; # Check the 'application' top-level key; its the only key that # is currently a HoH in the test configurations is_deeply $cfgr->config->{application}, { some_feature => 'bar', another_setting => 'baz', }, "full merging of configuration hashes"; } { my $cfgr = Dancer2::ConfigReader->new( environment => 'lconfig', location => $location2, default_config => { }, ); is_deeply $cfgr->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"; } done_testing; cookie.t100644000765000024 1663215154413402 14377 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; if ( Dancer2::Core::Cookie::_USE_XS ) { subtest 'with HTTP::XSCookies' => \&all_tests; no warnings 'redefine'; *Dancer2::Core::Cookie::to_header = \&Dancer2::Core::Cookie::pp_to_header; } else { diag "If you want extra speed, install HTTP::XSCookies"; } subtest 'w/o HTTP::XSCookies' => \&all_tests; sub all_tests { my $cookie = Dancer2::Core::Cookie->new( name => "foo" ); subtest "Constructor" => sub { isa_ok $cookie => 'Dancer2::Core::Cookie'; can_ok $cookie => 'to_header'; }; subtest "Setting values" => sub { 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";; is_deeply [ $cookie->values ] => [qw(a b c)], "values returns all values"; is scalar $cookie->values => 3, "values returns arrayref in scalar 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' ) ]; }; subtest "accessors and defaults" => sub { 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","None"\]/; }; 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", ); subtest "expiration strings" => sub { 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; for my $exp ( keys %times ) { my $want = $times{$exp}; $cookie->expires($exp); is $cookie->expires => $want, "expiry $exp => $want";; } }; subtest "to header" => sub { 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 => '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}; } }; subtest 'multi-value' => sub { 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/]; }; } done_testing; logger.t100644000765000024 536615154413402 14367 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse Test::More; use strict; use warnings; use Path::Tiny (); 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 engine 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, '<', Path::Tiny::path($dir, 'test')->stringify; 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; SECURITY.md100644000765000024 41315154413402 14215 0ustar00jasonstaff000000000000Dancer2-2.1.0If you need to report a security vulnerability in Dancer2, send all pertinent information to [dancer-security@dancer.pm](mailto:dancer-security@dancer.pm), or report it via the GitHub security tool. These reports will be addressed in the earliest possible timeframe. request.t100644000765000024 2216515154413402 14614 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; forward.t100644000765000024 1000215154413402 14553 0ustar00jasonstaff000000000000Dancer2-2.1.0/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/] Default content type charset', ); } # 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/] Default content type charset', ); } is( $cb->( GET '/forward_with_proxy/', 'X-Forwarded-Proto' => 'https' )->content, 'https://localhost/', '[GET /forward_with_proxy/] maintained is_behind_proxy', ); }; done_testing; factory.t100644000765000024 102015154413402 14536 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; dsl000755000765000024 015154413402 13333 5ustar00jasonstaff000000000000Dancer2-2.1.0/tpod.t100644000765000024 234215154413402 14443 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/dsluse strict; use warnings; use Test::More; use Dancer2::Core::DSL; use Pod::Simple::SimpleTree; use Ref::Util qw< is_arrayref >; { package App; use Dancer2; } my $dsl_keywords = Dancer2::Core::DSL->new(app => App->to_app)->dsl_keywords; isa_ok($dsl_keywords, 'HASH', 'Check whether keywords are present'); my $podpa = Pod::Simple::SimpleTree->new->parse_file('lib/Dancer2/Manual/Keywords.pod')->root; for my $entry (@$podpa) { if (ref($entry) eq 'ARRAY') { if ($entry->[0] eq 'head2') { # get the bare keyword and compare with the authoritative list my $title = $entry->[2]; my $keyword = is_arrayref($title) ? $title->[2] : $title; if (exists $dsl_keywords->{$keyword}) { $dsl_keywords->{$keyword}->{found} = $entry->[1]->{startline}; } } } } # go through the authoritative list and test we have a corresponding POD entry my %missing; for my $keyword ( sort keys %$dsl_keywords ) { exists $dsl_keywords->{$keyword}->{found} or $missing{$keyword}++; ok( exists $dsl_keywords->{$keyword}->{found}, "Keyword \"$keyword\" is documented in Dancer2::Manual::Keywords" ); } done_testing; print "$_\n" for keys %missing; app.t100644000765000024 53415154413402 14422 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 '/' ); any.t100644000765000024 221015154413402 14442 0ustar00jasonstaff000000000000Dancer2-2.1.0/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", ); } }; Makefile.PL100644000765000024 1546515154413402 14453 0ustar00jasonstaff000000000000Dancer2-2.1.0# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.037 use strict; use warnings; use 5.014; use ExtUtils::MakeMaker; 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", "CONFIGURE_REQUIRES" => { "CPAN::Meta::Requirements" => "2.120620", "ExtUtils::MakeMaker" => 0, "File::ShareDir::Install" => "0.06", "Module::Metadata" => 0 }, "DISTNAME" => "Dancer2", "EXE_FILES" => [ "script/dancer2" ], "LICENSE" => "perl", "MIN_PERL_VERSION" => "5.014", "NAME" => "Dancer2", "PREREQ_PM" => { "Attribute::Handlers" => 0, "CLI::Osprey" => 0, "Carp" => 0, "Clone" => 0, "Config::Any" => 0, "Data::Censor" => "0.04", "Digest::SHA" => 0, "Encode" => 0, "Exporter" => "5.57", "Exporter::Tiny" => 0, "File::Copy" => 0, "File::Path" => 0, "File::Share" => 0, "File::Temp" => 0, "File::Which" => 0, "HTTP::Date" => 0, "HTTP::Headers::Fast" => "0.21", "HTTP::Tiny" => 0, "Hash::Merge::Simple" => 0, "Hash::MultiValue" => 0, "Import::Into" => 0, "JSON::MaybeXS" => 0, "List::Util" => "1.29", "MIME::Base64" => "3.13", "Module::Pluggable" => 0, "Module::Runtime" => 0, "Moo" => "2.000000", "Moo::Role" => 0, "POSIX" => 0, "Path::Tiny" => 0, "Plack" => "1.0040", "Plack::Middleware::FixMissingBodyInRedirect" => 0, "Plack::Middleware::RemoveRedundantBody" => 0, "Ref::Util" => 0, "Role::Tiny" => "2.000000", "Safe::Isa" => 0, "Sub::Quote" => 0, "Sub::Util" => 0, "Template" => 0, "Template::Tiny" => "1.16", "Test::Builder" => 0, "Test::More" => "0.92", "Type::Tiny" => "1.000006", "Types::Standard" => 0, "URI::Escape" => 0, "YAML" => "0.86", "parent" => 0 }, "TEST_REQUIRES" => { "Capture::Tiny" => "0.12", "ExtUtils::MakeMaker" => 0, "File::Spec" => 0, "HTTP::Cookies" => 0, "HTTP::Headers" => 0, "IO::Handle" => 0, "IPC::Open3" => 0, "Pod::Simple::SimpleTree" => 0, "Template" => 0, "Test::Builder" => 0, "Test::EOL" => 0, "Test::Exception" => 0, "Test::Fatal" => 0, "Test::More" => "0.92" }, "VERSION" => "2.1.0", "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-HasConfig/*.t t/classes/Dancer2-Core-Role-HasEnvironment/*.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/examples/*.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-1449/*.t t/issues/gh-1621/*.t t/issues/gh-1664/*.t t/issues/gh-1712/*.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/plugin2/app_dsl_cb/*.t t/roles/*.t t/route-pod-coverage/*.t t/scope_problems/*.t" } ); my %FallbackPrereqs = ( "Attribute::Handlers" => 0, "CLI::Osprey" => 0, "Capture::Tiny" => "0.12", "Carp" => 0, "Clone" => 0, "Config::Any" => 0, "Data::Censor" => "0.04", "Digest::SHA" => 0, "Encode" => 0, "Exporter" => "5.57", "Exporter::Tiny" => 0, "ExtUtils::MakeMaker" => 0, "File::Copy" => 0, "File::Path" => 0, "File::Share" => 0, "File::Spec" => 0, "File::Temp" => 0, "File::Which" => 0, "HTTP::Cookies" => 0, "HTTP::Date" => 0, "HTTP::Headers" => 0, "HTTP::Headers::Fast" => "0.21", "HTTP::Tiny" => 0, "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::Pluggable" => 0, "Module::Runtime" => 0, "Moo" => "2.000000", "Moo::Role" => 0, "POSIX" => 0, "Path::Tiny" => 0, "Plack" => "1.0040", "Plack::Middleware::FixMissingBodyInRedirect" => 0, "Plack::Middleware::RemoveRedundantBody" => 0, "Pod::Simple::SimpleTree" => 0, "Ref::Util" => 0, "Role::Tiny" => "2.000000", "Safe::Isa" => 0, "Sub::Quote" => 0, "Sub::Util" => 0, "Template" => 0, "Template::Tiny" => "1.16", "Test::Builder" => 0, "Test::EOL" => 0, "Test::Exception" => 0, "Test::Fatal" => 0, "Test::More" => "0.92", "Type::Tiny" => "1.000006", "Types::Standard" => 0, "URI::Escape" => 0, "YAML" => "0.86", "parent" => 0 ); # inserted by Dist::Zilla::Plugin::DynamicPrereqs 0.040 if (has_module('HTTP::XSCookies')) { requires('HTTP::XSCookies', '0.000007') } 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); } # inserted by Dist::Zilla::Plugin::DynamicPrereqs 0.040 sub _add_prereq { my ($mm_key, $module, $version_or_range) = @_; $version_or_range ||= 0; warn "$module already exists in $mm_key (at version $WriteMakefileArgs{$mm_key}{$module}) -- need to do a sane metamerge!" if exists $WriteMakefileArgs{$mm_key}{$module} and $WriteMakefileArgs{$mm_key}{$module} ne '0' and $WriteMakefileArgs{$mm_key}{$module} ne $version_or_range; warn "$module already exists in FallbackPrereqs (at version $FallbackPrereqs{$module}) -- need to do a sane metamerge!" if exists $FallbackPrereqs{$module} and $FallbackPrereqs{$module} ne '0' and $FallbackPrereqs{$module} ne $version_or_range; $WriteMakefileArgs{$mm_key}{$module} = $FallbackPrereqs{$module} = $version_or_range; return; } sub has_module { my ($module, $version_or_range) = @_; require Module::Metadata; my $mmd = Module::Metadata->new_from_module($module); return undef if not $mmd; return $mmd->version($module) if not defined $version_or_range; require CPAN::Meta::Requirements; my $req = CPAN::Meta::Requirements->new; $req->add_string_requirement($module => $version_or_range); return 1 if $req->accepts_module($module => $mmd->version($module)); return 0; } sub requires { goto &runtime_requires } sub runtime_requires { my ($module, $version_or_range) = @_; _add_prereq(PREREQ_PM => $module, $version_or_range); } utf8_url.t100644000765000024 104315154413402 14644 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse utf8; use strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Encode; my $utf8 = 'ľščťžýáí'; { package MyApp; use Dancer2; use utf8; get '/ľščťžýáí' => sub { return 'ľščťžýáí'; }; } my $app = Dancer2->psgi_app; test_psgi $app, sub { my $cb = shift; my $res = $cb->( GET '/ľščťžýáí' ); is $res->code, 200, 'route succeeded'; is Encode::decode('utf8', $res->content), 'ľščťžýáí', 'Correct content'; }; done_testing(); response.t100644000765000024 257215154413402 14742 0ustar00jasonstaff000000000000Dancer2-2.1.0/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, [ '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 } ), 10; # 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; redirect.t100644000765000024 652015154413402 14702 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' }; get '/redirect_uriescaped' => sub { redirect '?foo=bar+%26+baz' }; } 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'), '/', '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'), '/login?failed=1', 'Correct Location header', ); } { my $res = $cb->( GET '/redirect_uriescaped' ); is( $res->code, 302, '[GET /redirect_uriescaped] Correct code' ); is( $res->headers->header('Location'), '?foo=bar+%26+baz', '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'), '/absolute', 'Correct Location header', ); } { my $res = $cb->( GET '/relative' ); is( $res->headers->header('Location'), 'somewhere/else', 'Correct Location header', ); } }; }; done_testing; config.yml100644000765000024 12415154413402 14656 0ustar00jasonstaff000000000000Dancer2-2.1.0/tlog: "info" logger: "Note" strict_config: 0 plugins: "FooPlugin": plugin: 42 psgi_app.t100644000765000024 436315154413402 14706 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 ); }; } template.t100644000765000024 1273515154413402 14741 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use Dancer2::Core::Hook; use Plack::Test; use HTTP::Request::Common; use Path::Tiny qw< path >; use lib 't/lib'; eval { require Template; Template->import(); 1 } or plan skip_all => 'Template::Toolkit probably missing.'; use_ok('Dancer2::Template::TemplateToolkit'); use_ok('Dancer2::Template::TemplateToolkitFoo'); my $views = path( __FILE__ )->parent->child('views')->absolute->stringify; my $tt = Dancer2::Template::TemplateToolkit->new( views => $views, layout => 'main.tt', layout_dir => 'layouts', ); 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'; }; # Test that a custom class can be used for Template::Toolkit my $tt_custom = Dancer2::Template::TemplateToolkitFoo->new; isa_ok $tt_custom, 'Dancer2::Template::TemplateToolkit'; isa_ok $tt_custom, 'Dancer2::Template::TemplateToolkitFoo'; { package CustomFoo; use Dancer2; Dancer2->runner->apps->[1]->set_template_engine($tt_custom); get '/' => sub { template 'index' }; } subtest 'custom template render' => sub { my $test = Plack::Test->create( CustomFoo->to_app ); my $res = $test->request( GET '/' ); is $res->content, 'Custom Render Template', 'Custom Render Template'; }; { 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' }; get '/default_layout_dir' => sub { app->template_engine->layout_dir }; get '/set_layout_dir_via_settings' => sub { set layout_dir => 'alt_layout' }; get '/get_layout_dir_via_settings' => sub { set 'layout_dir' }; } 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', ); }; subtest "modify layout_dir" => sub { my $test = Plack::Test->create( Foo->to_app ); is( $test->request( GET '/default_layout_dir' )->content, 'layouts', '[GET /default_layout_dir] Correct layout dir', ); # trigger a test via a route $test->request( GET '/set_layout_dir_via_settings' ); is( $test->request( GET '/get_layout_dir_via_settings' )->content, 'alt_layout', '[GET /get_layout_dir_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; lib000755000765000024 015154413402 13317 5ustar00jasonstaff000000000000Dancer2-2.1.0/tpoc.pm100644000765000024 43215154413402 14555 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; Foo.pm100644000765000024 12615154413402 14517 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/libpackage t::lib::Foo; use Dancer2; get '/in_foo' => sub { session('test'); }; 1; mime.t100644000765000024 177115154413402 14615 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/dsluse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App::MIME; use Dancer2; # set some MIME aliases... mime->add_type( foo => 'text/foo' ); mime->add_alias( f => 'foo' ); # Set default mime type set 'default_mime_type' => 'text/bar'; # test static corpus set static_handler => 1; set public_dir => 't/corpus/static'; # added type get '/foo' => sub { send_file 'empty.foo'; }; } my $app = Plack::Test->create( App::MIME->to_app ); subtest 'send_file content type' => sub { my $res = $app->request( GET '/foo' ); ok( $res->is_success, 'Successful request' ); is( $res->content_type, 'text/foo', '.. and correct mime type'); }; # Ref: #1546 subtest 'static handler content type' => sub { my $res = $app->request( GET '/empty.foo' ); ok( $res->is_success, 'Successful request via static handler' ); is( $res->content_type, 'text/foo', '.. and correct mime type'); }; halt.t100644000765000024 333515154413402 14614 0ustar00jasonstaff000000000000Dancer2-2.1.0/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->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; path.t100644000765000024 634715154413402 14626 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.t100644000765000024 67215154413402 14616 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.t100644000765000024 174315154413402 14633 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; yaml.t100644000765000024 65415154413402 14607 0ustar00jasonstaff000000000000Dancer2-2.1.0/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( to_yaml(\%test), "---\nfoo: bar\n", 'to_yaml works' ); ::is_deeply( from_yaml(to_yaml(\%test)), \%test, 'from_yaml works' ); }; } Plack::Test->create( App->to_app )->request( GET '/' ); app_alone.t100644000765000024 63415154413402 15017 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' ); }; auto_page.t100644000765000024 462515154413402 15051 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; App2.pm100644000765000024 25715154413402 14603 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.pm100644000765000024 65415154413402 14645 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.pm100644000765000024 25715154413402 14602 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.t100644000765000024 120515154413402 15001 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' ); named_apps.t100644000765000024 136315154413402 15210 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; file_utils.t100644000765000024 336615154413402 15245 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use utf8; use Test::More tests => 14; use Test::Fatal; use Path::Tiny (); 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/^Error open.+No such file or directory/, 'Failure opening nonexistent file', ); my $content = Dancer2::FileUtils::read_file_content(); is $content, undef; 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), Path::Tiny::path("$tmpdir")->stringify, 'path_or_empty on an existing path' ); 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]; } } log_levels.t100644000765000024 656215154413402 15242 0ustar00jasonstaff000000000000Dancer2-2.1.0/t#!perl use strict; use warnings; use Test::More tests => 8; 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'; }; get '/engine-warning' => sub { # Ensure that the logger and warining level is going to be used by the engines, not just the application code # Also ensure that the current log level, not the log level when the serialiser is created, is what counts. set log => 'debug'; set serializer => 'JSON'; set template => 'Tiny'; set session => 'Simple'; set log => 'warning'; foreach my $engine (qw(serializer session template)) { app->engine($engine)->log_cb->($_ => "$engine $_ msg\n") for qw(debug warning error); } return ["engine-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', ); } { my $stderr = capture_stderr { $res = $cb->( GET '/engine-warning' ) }; is( $res->code, 200, 'Successful response' ); like( $stderr, qr/ ^ # serializer engine should output warning and error only \[App:\d+\] \s warning [^\n]+? serializer \s warning [^\n]+ \n \[App:\d+\] \s error [^\n]+? serializer \s error [^\n]+ \n # session engine should output warning and error only \[App:\d+\] \s warning [^\n]+? session \s warning [^\n]+ \n \[App:\d+\] \s error [^\n]+? session \s error [^\n]+ \n # template engine should output warning and error only \[App:\d+\] \s warning [^\n]+? template \s warning [^\n]+ \n \[App:\d+\] \s error [^\n]+? template \s error [^\n]+ \n $ /x, 'Log levels work', ); } }; serializer.t100644000765000024 164315154413402 15253 0ustar00jasonstaff000000000000Dancer2-2.1.0/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', ); dispatcher.t100644000765000024 1440115154413402 15244 0ustar00jasonstaff000000000000Dancer2-2.1.0/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_stacktrace => 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', ], ["home"], $simple_route, ] }, { env => { REQUEST_METHOD => 'GET', PATH_INFO => '/user/Johnny', }, expected => [ 200, [ 'Content-Length' => 12, 'Content-Type' => 'text/html; charset=utf-8', ], ["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' ], [], $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', ], 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"; custom_dsl.t100644000765000024 105015154413402 15246 0ustar00jasonstaff000000000000Dancer2-2.1.0/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(); multi_apps.t100644000765000024 177715154413402 15267 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; roles000755000765000024 015154413402 13675 5ustar00jasonstaff000000000000Dancer2-2.1.0/thook.t100644000765000024 263115154413402 15164 0ustar00jasonstaff000000000000Dancer2-2.1.0/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'; to_app.t100644000765000024 103615154413402 15142 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' ); }; } extend.t100644000765000024 203515154413402 15147 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 Path::Tiny qw< path >; BEGIN { $ENV{DANCER_CONFDIR} = path( $FindBin::Bin, 'extend_config' )->stringify; } use Dancer2; ok(defined &foo, 'config specified DSL can foo'); is(foo('baz'), 'baz', 'config specified Foo returns baz'); done_testing; xt000755000765000024 015154413402 12741 5ustar00jasonstaff000000000000Dancer2-2.1.0perltidy.rc100644000765000024 224215154413402 15263 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 script000755000765000024 015154413402 13612 5ustar00jasonstaff000000000000Dancer2-2.1.0dancer2100755000765000024 564115154413402 15224 0ustar00jasonstaff000000000000Dancer2-2.1.0/script#!/usr/bin/env perl # PODNAME: dancer2 # ABSTRACT: Dancer2 command line interface use strict; use warnings; use Dancer2::CLI; Dancer2::CLI->new_with_options->run; __END__ =pod =encoding UTF-8 =head1 NAME dancer2 - Dancer2 command line interface =head1 VERSION version 2.1.0 =head1 SYNOPSIS dancer2 [options...] =head1 DESCRIPTION (If you're looking for the main L module documentation via CLI, try C.) 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 a new Dancer2 application. =item * version Display version of Dancer2 currently installed. =back To get detailed description of each individual command run: dancer2 --help The latest list of available commands can be displayed by: dancer2 =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2021 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. =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 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 lib000755000765000024 015154413402 13054 5ustar00jasonstaff000000000000Dancer2-2.1.0Dancer2.pm100644000765000024 2464515154413402 15063 0ustar00jasonstaff000000000000Dancer2-2.1.0/libpackage Dancer2; $Dancer2::VERSION = '2.1.0'; # ABSTRACT: Lightweight yet powerful web application framework use 5.12.0; 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; 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 2.1.0 =head1 DESCRIPTION (If you're looking for the L command documentation via CLI, try C.) 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 (via L), enabling you to easily deploy Dancer2 applications on hosts that do not support custom CPAN modules. Creating web applications with Dancer2 is easy and fun: #!/usr/bin/env perl package HelloWorld; use Dancer2; get '/' => sub { return "Hello, world!"; }; true; HelloWorld->to_app; This is the main module for the Dancer2 distribution. It contains logic for creating a new Dancer2 application. =head2 Documentation Index You have questions. We have answers. =over 4 =item * Dancer2 Tutorial Want to learn by example? The L will take you from installation to a working application. item * Quick Start Want to get going faster? L will help you install Dancer2 and bootstrap a new application quickly. =item * Dancer2 Manual Want to gain understanding of Dancer2 so you can use it best? The L is a comprehensive guide to the framework. =item * Dancer2 Keywords Looking for list of all the keywords? The L documents the entire Dancer2 DSL. =item * Dancer2 Config Need to fine tune your application? The L is the complete reference to all configuration options. =item * Dancer2 Deployment Ready to get your application off the ground? L helps you deploy your application to a real-world host. =item * Dancer2 Cookbook How do I...? Our L comes with various recipes in many tasty flavors! =item * Dancer2 Plugins Looking for add-on functionality for your application? The L contains our curated list of recommended plugins. For information on how to author a plugin, see L. =item * Dancer2 Migration guide Starting from Dancer 1? Jump over to the L to learn how to make the smoothest transition to Dancer2. =back =head3 Other Documentation =over =item * Core and Community Policy, and Standards of Conduct The L defines what constitutes acceptable behavior in our community, what behavior is considered abusive and unacceptable, and what steps will be taken to remediate inappropriate and abusive behavior. By participating in any public forum for Dancer or its community, you are agreeing to the terms of this policy. =item * GitHub Wiki Our L has community-contributed documentation, as well as other information that doesn't quite fit within this manual. =item * Contributing The L describe how to set up your development environment to contribute to the development of Dancer2, Dancer2's Git workflow, submission guidelines, and various coding standards. =item * Deprecation Policy The L defines the process for removing old, broken, unused, or outdated code from the Dancer2 codebase. This policy is critical for guiding and shaping future development of Dancer2. =back =head1 SECURITY REPORTS If you need to report a security vulnerability in Dancer2, send all pertinent information to L, or report it via the GitHub security tool. These reports will be addressed in the earliest possible timeframe. =head1 SUPPORT 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. =head1 AUTHORS =head2 CORE DEVELOPERS Alberto Simões Alexis Sukrieh D Ruth Holloway (GeekRuthie) Damien Krotkine David Precious Franck Cuny Jason A. Crome Mickey Nasriachi Peter Mottram (SysPete) Russell Jenkins Sawyer X Stefan Hornburg (Racke) Yanick Champoux =head2 CORE DEVELOPERS EMERITUS David Golden Steven Humphrey =head2 CONTRIBUTORS A. Sinan Unur Abdullah Diab Achyut Kumar Panda Ahmad M. Zawawi Alex Beamish Alexander Karelas Alexander Pankoff Alexandr Ciornii Andrew Beverley Andrew Grangaard Andrew Inishev Andrew Solomon Andy Jack Ashvini V B10m Bas Bloemsaat baynes Ben Hutton Ben Kaufman biafra Blabos de Blebe Breno G. de Oliveira cdmalon Celogeek Cesare Gargano Charlie Gonzalez chenchen000 Chi Trinh Christian Walde Christopher White cloveistaken Colin Kuskie cym0n Dale Gallagher Dan Book (Grinnz) Daniel Böhmer Daniel Muey Daniel Perrett Dave Jacoby Dave Webb David (sbts) David Steinbrunner David Zurborg Davs Deirdre Moran Dennis Lichtenthäler Dinis Rebolo dtcyganov Elliot Holden Emil Perhinschi Erik Smit Fayland Lam ferki Gabor Szabo GeekRuthie geistteufel Gideon D'souza Gil Magno Glenn Fowler Graham Knop Gregor Herrmann Grzegorz Rożniecki Hobbestigrou Hunter McMillen ice-lenor icyavocado Ivan Bessarabov Ivan Kruglov JaHIY Jakob Voss James Aitken James Raspass James McCoy Jason Lewis Javier Rojas Jean Stebens Jens Rehsack Joel Berger Johannes Piehler Jonathan Cast Jonathan Scott Duff Joseph Frazer Julien Fiegehenn (simbabque) Julio Fraire Kaitlyn Parkhurst (SYMKAT) Karen Etheridge 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 mauke Maurice MaxPerl Ma_Sys.ma Menno Blom Michael Kröll Michał Wojciechowski Mike Katasonov Mikko Koivunalho Mohammad S Anwar mokko Nick Patch Nick Tonkin Nigel Gregoire Nikita K Nuno Carvalho Olaf Alders Olivier Mengué Omar M. Othman pants Patrick Zimmermann Pau Amma Paul Clements Paul Cochrane Paul Williams Pedro Bruno Pedro Melo Philippe Bricout Ricardo Signes Rick Yakubowski Ruben Amortegui Sakshee Vijay (sakshee3) Sam Kington Samit Badle Sebastien Deseille (sdeseille) Sergiy Borodych Shlomi Fish Slava Goltser Snigdha Sorin Pop Steve Bertrand Steve Dondley Steven Humphrey Tatsuhiko Miyagawa Timothy Alexis Vass Tina Müller Tom Hukins Upasana Shukla Utkarsh Gupta Vernon Lyon Victor Adam Vince Willems Vincent Bachelier xenu Yves Orton =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 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.t100644000765000024 1111515154413402 14762 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; # this test was generated with Dist::Zilla::Plugin::Test::Compile 2.059 use Test::More; plan tests => 61 + ($ENV{AUTHOR_TESTING} ? 1 : 0); my @module_files = ( 'Dancer2.pm', 'Dancer2/CLI.pm', 'Dancer2/CLI/Gen.pm', 'Dancer2/CLI/Version.pm', 'Dancer2/ConfigReader.pm', 'Dancer2/ConfigReader/Config/Any.pm', 'Dancer2/ConfigUtils.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/HasConfig.pm', 'Dancer2/Core/Role/HasEnvironment.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/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*(?:env )?perl\S*)((?:\s+-\w*)*)(?:\s*#.*)?$/; @switches = (@switches, split(' ', $1)) if $1; close $fh and skip("$file uses -T; not testable with PERL5LIB", 1) if grep $_ eq '-T', @switches and $ENV{PERL5LIB}; 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}; CONTRIBUTING.md100644000765000024 2556515154413402 14734 0ustar00jasonstaff000000000000Dancer2-2.1.0# 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 or duplicated effort, and to maximize the chances of your contributions being used. There are many ways to contribute to the project beyond what is in this document. Dancer2 is an active project and any kind of help is very much appreciated! ### Asking Questions Yes, asking questions contributes to our overall community! Please use IRC or StackOverflow if possible. If these other options don't work for you, or it's taking too long to get an answer, you can leave a question on our [GitHub issues page](https://github.com/PerlDancer/Dancer2/issues). ### Reporting Bugs We prefer to have all our bug reports on GitHub, in the [issues section](https://github.com/PerlDancer/Dancer2/issues). Please make sure the bug you're reporting does not yet exist. If in doubt please ask on IRC. When reporting an issue, please include the following information in your post: - Describe what you expected to happen. - Describe what actually happened. - If possible, include a minimal reproducible example of the problem to help us identify and fix the issue. This also helps us make sure it's not an issue with your code. - List the Perl and Dancer2 versions you are using. If you can, see if this has already been reported and fixed with the latest code in GitHub. ### Improve 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)). ### Writing 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. ### Blog or Write Articles Some of the best ways to spread the word about Dancer2 is to blog or write articles. Consider DEV.to, Medium, your personal blog, or even the [Perl blogs site](https://blogs.perl.org). ## 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.14, 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. Just because the minimum supported version of Perl changes doesn't mean that existing code should immediately be rewritten to include new language features. For new code, or code that is being refactored for other reasons, it is acceptable to use language features that are new to the minimum supported version of Perl supported by the Dancer Core Team. 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](https://www.cpantesters.org). A good way to help the project is to find a failing build log on the [CPAN testers](https://www.cpantesters.org/distro/D/Dancer2.html). If you find a failing test report, feel free to report it as a [GitHub issue](https://github.com/PerlDancer/Dancer2/issues). ## 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 ... 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/yourname/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 _main_ branch. Therefore, all your contribution work should be done in a fork of the _main_ branch. Here is the workflow for submitting a patch: 1. Fork the repository: https://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/yourname/Dancer2.git 3. As a contributor, you should **always** work on the `main` 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 _main_ and that will track the official _main_ 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 main 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. Run the test suite! Pull requests with failing tests will not be considered. You have two ways to run tests, either with prove: $ prove -lvr t/ or via _Dist::Zilla_: $ dzil test --all 6. 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](https://www.perldancer.org/irc). 7. When the core team reviews your pull request, it will either accept (and then merge into _main_) 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 _main_, then all you have to do is remove your local and remote _pr/$name_ branch: $ git checkout main $ 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 main $ git push origin main You're now ready to start working on a new pull request! ### Pull Request Review Guidelines This is a non-exhaustive list of things that may cause the Dancer Core Team to request modifications to (or reject) your pull request: - Insufficient documentation - Failing tests in the test suite - Lack of tests provided covering new/changed functionality - Merge conflicts in git - Poor commit messages - Code or feature does not fit with the Core Team's vision or values for Dancer - Code introduces new bugs, or does not completely resolve an issue config_many.t100644000765000024 207415154413402 15372 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; BEGIN { # undefine ENV vars used as defaults for app environment in these tests local $ENV{DANCER_ENVIRONMENT}; local $ENV{PLACK_ENV}; $ENV{DANCER_CONFDIR} = './t/app/t1'; } use lib '.'; use lib './t/lib'; use Dancer2::Core::App; subtest basic => sub { $ENV{DANCER_CONFIG_READERS} = 'Dancer2::ConfigReader::Config::Any,Dancer2::ConfigReader::TestDummy'; my $app = Dancer2::Core::App->new( name => 'basic' ); is $app->config->{app}->{config}, 'ok', $app->name . ": config loaded properly"; is $app->config->{dummy}->{dummy_subitem}, 2, $app->name . ": dummy config loaded properly"; }; subtest additional_config_readers => sub { $ENV{DANCER_CONFIG_READERS} = 'Dancer2::ConfigReader::Additional'; my $app = Dancer2::Core::App->new( name => 'additional' ); is $app->config->{app}->{config}, 'ok', $app->name . ": config loaded properly"; is $app->config->{dummy}->{dummy_subitem}, 2, $app->name . ": dummy config loaded properly"; }; done_testing; http_status.t100644000765000024 275715154413402 15473 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.t100644000765000024 1021515154413402 15326 0ustar00jasonstaff000000000000Dancer2-2.1.0/t# who test the tester? We do! use strict; use warnings; use Ref::Util qw; use Path::Tiny (); 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} = Path::Tiny::path( __FILE__ )->parent->child('dancer-test')->stringify; } use Test::More tests => 46; 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; ## 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 decode( 'UTF-8', $param_response->content ), 'test/' . $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(); } deserialize.t100644000765000024 1507015154413402 15421 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More tests => 17; 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'; # deserialization fail hook hook 'core.error.before' => sub { my ($err) = @_; # Dancer2::Core::Error object if ( $err->message =~ m!Failed to deserialize content! ) { $err->status(444); # custom status } }; 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; my $r = $cb->( PUT '/from_params', 'Content-Type' => 'application/json', Content => '---', ); # Ensure error is logged 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' ); my $err_regex = qr{ ^ \QFailed to deserialize content: \E \Qmalformed number\E }x; is( $msg->{'level'}, 'core', 'Correct level' ); like( $msg->{'message'}, $err_regex, 'Logged correct error message' ); # Check we get a 444 response is( $r->code, 444, "444 custom response" ); my $content = Dancer2::Serializer::JSON::decode_json( $r->content ); like( $content->{message}, $err_regex, "Failed to deserialize content error"); } } prepare_app.t100644000765000024 201015154413402 15365 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Plack::Test; use HTTP::Request::Common; use Test::More 'tests' => 2; { package Foo; use Dancer2; prepare_app { set 'app_1' => 'called 1'; }; get '/' => sub {'OK'}; } { package Bar; use Dancer2 'appname' => 'Foo'; prepare_app { set 'app_2' => 'called 2'; }; get '/' => sub {'OK'}; } subtest 'Foo' => sub { my $app = Foo->to_app; is( Foo->config()->{'app_1'}, 'called 1', 'App 1 had prepare_app called', ); my $test = Plack::Test->create($app); my $res = $test->request( GET '/' )->content(); is( $res, 'OK', 'Correct content', ); }; subtest 'Bar' => sub { my $app = Bar->to_app; is( Bar->config()->{'app_2'}, 'called 2', 'App 2 had prepare_app called', ); my $test = Plack::Test->create($app); my $res = $test->request( GET '/' )->content(); is( $res, 'OK', 'Correct content', ); }; utf8_strict.t100644000765000024 423715154413402 15362 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Encode qw(encode); use Dancer2::Core::Request; use Dancer2::Serializer::JSON; subtest 'request path decoding (lenient)' => sub { my $bytes = encode( 'UTF-8', "/\x{00F8}" ); my $req = Dancer2::Core::Request->new( env => { REQUEST_METHOD => 'GET', PATH_INFO => $bytes, }, ); my $path = $req->path; is( ord( substr( $path, 1, 1 ) ), 0xF8, 'decoded UTF-8 path' ); ok( utf8::is_utf8($path), 'decoded path is characters' ); }; subtest 'request path decoding (invalid utf8, lenient)' => sub { my $bad = '/' . pack( 'C', 0xFF ); my $req = Dancer2::Core::Request->new( env => { REQUEST_METHOD => 'GET', PATH_INFO => $bad, }, ); my @warnings; local $SIG{__WARN__} = sub { push @warnings, @_ }; my $path = $req->path; is( $path, $bad, 'invalid UTF-8 left as bytes' ); like( $warnings[0], qr/Invalid UTF-8/, 'warned on invalid UTF-8' ); }; subtest 'request path decoding (invalid utf8, strict)' => sub { my $req = Dancer2::Core::Request->new( env => { REQUEST_METHOD => 'GET', PATH_INFO => '/' . pack( 'C', 0xFF ), }, strict_utf8 => 1, ); my $error = exception { $req->path }; like( $error, qr/Invalid UTF-8/, 'strict mode rejects invalid UTF-8' ); }; subtest 'json serialization (lenient + strict)' => sub { my $bad = pack( 'C', 0xFF ); my @warnings; my $serializer = Dancer2::Serializer::JSON->new( config => { strict_utf8 => 0 }, log_cb => sub { push @warnings, $_[1] }, ); my $json = $serializer->serialize( { bad => $bad } ); ok( $json, 'serialize succeeded in lenient mode' ); like( $warnings[0], qr/Invalid UTF-8/, 'warned on invalid UTF-8' ); my $strict_bad = pack( 'C', 0xFF ); my $strict = Dancer2::Serializer::JSON->new( config => { strict_utf8 => 1 }, log_cb => sub {1}, ); my $error = exception { $strict->serialize( { bad => $strict_bad } ) }; like( $error, qr/Invalid UTF-8/, 'strict mode rejects invalid UTF-8' ); }; done_testing(); content.t100644000765000024 254215154413402 15335 0ustar00jasonstaff000000000000Dancer2-2.1.0/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_stacktrace => 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', ); }; send_as.t100644000765000024 1001115154413402 15305 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 '/plain' => sub { send_as plain => 'some plain text with '; }; 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 plain" => sub { my $res = $test->request( GET '/plain' ); is $res->code, '200'; is $res->content_type, 'text/plain'; is $res->content_type_charset, 'UTF-8'; is $res->content, 'some plain text with '; }; 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"; } { local $SIG{__WARN__} = sub { return if $_[0] =~ /Subroutine .* redefined/; CORE::warn @_; }; 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(); request.t100644000765000024 211315154413402 15345 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 keyword' => 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.t100644000765000024 1150715154413402 15313 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 => 6; { 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::AddHeader; ## no critic use Dancer2; hook 'after' => sub { my $response = shift; $response->push_header('Authorization' => 'bar'); }; get '/' => sub { delayed { response->push_header('Content-Type' => 'foo'); flush; content "baz"; done; } }; } { 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 has push_header method' => sub { my $test = Plack::Test->create( App::AddHeader->to_app ); my $res = $test->request( GET '/' ); ok( $res->is_success, 'Successful request' ); is( $res->headers->header('Content-Type'), 'foo', 'Correct header pushed from inside "delayed"' ); is( $res->headers->header('Authorization'), 'bar', 'Correct header pushed from inside hook' ); }; 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' ); } }; uri_for.t100644000765000024 217615154413402 15333 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/dsluse strict; use warnings; use Test::More 'tests' => 2; use Plack::Test; use Plack::Builder; use HTTP::Request::Common; { package App; use Dancer2; get '/' => sub { return uri_for('/foo'); }; } { package MountedApp; use Dancer2; get '/' => sub { return uri_for('/bar'); }; } my $prefix = 'http://localhost'; subtest 'Non-mounted app' => sub { my $app = Plack::Test->create( App->to_app ); my $res; $res = $app->request( GET "$prefix/" ); ok( $res->is_success, 'Successful request' ); is( $res->content, "$prefix/foo", 'Correct regular path' ); }; subtest 'Mounted app' => sub { my $app = Plack::Test->create( builder { mount '/mount' => MountedApp->to_app; mount '/' => App->to_app; } ); my $res; $res = $app->request( GET "$prefix/" ); ok( $res->is_success, 'Successful request' ); is( $res->content, "$prefix/foo", 'Correct mounted regular path' ); $res = $app->request( GET "$prefix/mount" ); ok( $res->is_success, 'Successful request' ); is($res->content, "$prefix/mount/bar", 'Correct mounted regular path'); }; whitespace.t100644000765000024 31215154413402 15376 0ustar00jasonstaff000000000000Dancer2-2.1.0/xtuse Test::Whitespaces { dirs => [qw( lib script t tools xt )], ignore => [ qr{t/sessions/}, qr{t/template_tiny/samples}, ], }; named_routes.t100644000765000024 432015154413402 15562 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More 'tests' => 5; use Plack::Test; use HTTP::Request::Common; { package MyApp; use Dancer2; # Name, Regexp, Code get 'view_static', '/view' => sub { 'View Static'; }; get 'view_regex', qr{^/view_r$} => sub { 'View Regex' }; # Name, Regexp, Options, Code get 'base_static', '/' => { 'user_agent' => 'XX' }, sub { 'Base Static'; }; # Name, Regexp, Options, Code get 'base_regex', qr{^/r$}, {}, sub { 'Base Regex'; }; get '/ignore1' => sub {1}; get '/ignore2' => sub {1}; get '/ignore3' => sub {1}; } my $test = Plack::Test->create( MyApp->to_app ); subtest 'Named static route' => sub { plan 'tests' => 2; my $response = $test->request( GET '/view' ); ok( $response->is_success, 'Successfully reached /view' ); is( $response->content, 'View Static', 'Static route with name' ); }; subtest 'Named regex route' => sub { plan 'tests' => 2; my $response = $test->request( GET '/view_r' ); ok( $response->is_success, 'Successfully reached /view_r' ); is( $response->content, 'View Regex', 'Regex route with name' ); }; subtest 'Named static route with options' => sub { plan 'tests' => 2; my $response = $test->request( GET '/', 'User-Agent' => 'XX' ); ok( $response->is_success, 'Successfully reached /' ); is($response->content, 'Base Static', 'Static route with name and options'); }; subtest 'Named regex route with options' => sub { plan 'tests' => 2; my $response = $test->request( GET '/r', 'User-Agent' => 'XX' ); ok( $response->is_success, 'Successfully reached /r' ); is($response->content, 'Base Regex', 'Regex route with name and options'); }; subtest 'Route objects' => sub { plan 'tests' => 3; my @apps = @{ Dancer2::runner->apps }; is( scalar @apps, 1, 'Only one app exists' ); my %routes = %{ $apps[0]->route_names() }; is( scalar keys %routes, 4, 'Four named routes registered' ); is_deeply( [ sort keys %routes ], [ 'base_regex', 'base_static', 'view_regex', 'view_static', ], 'All the right route names', ); }; config_utils.t100644000765000024 63615154413402 15550 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Dancer2::ConfigUtils qw/normalize_config_entry/; is( normalize_config_entry( 'charset', 'UTF-8' ), 'utf-8', 'normalized UTF-8 to utf-8'); like( exception { normalize_config_entry( 'charset', 'BOGUS' ) }, qr{Charset defined in configuration is wrong : couldn't identify 'BOGUS'}, 'Configuration file charset failure', ); done_testing; http_methods.t100644000765000024 246015154413402 15602 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 ); } }; template_ext.t100644000765000024 115615154413402 15574 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; t2000755000765000024 015154413402 13656 5ustar00jasonstaff000000000000Dancer2-2.1.0/t/app.dancer100644000765000024 115154413402 15162 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t2 TestPod.pm100644000765000024 112715154413402 15400 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; SubApp2.pm100644000765000024 25315154413402 15251 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; SubApp1.pm100644000765000024 25315154413402 15250 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; TestApp.pm100644000765000024 476615154413402 15412 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; views000755000765000024 015154413402 13706 5ustar00jasonstaff000000000000Dancer2-2.1.0/tindex.tt100644000765000024 20115154413402 15477 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/views[index] var = [% var %] before_layout_render = [% before_layout_render %] before_template_render = [% before_template_render %] perlcritic.rc100644000765000024 464315154413402 15576 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 popping 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] share000755000765000024 015154413402 13410 5ustar00jasonstaff000000000000Dancer2-2.1.0.gitignore100644000765000024 5015154413402 15473 0ustar00jasonstaff000000000000Dancer2-2.1.0/sharesessions/ logs/ *_local.* environments/ memory_cycles.t100644000765000024 111215154413402 15743 0ustar00jasonstaff000000000000Dancer2-2.1.0/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(); plugin_import.t100644000765000024 236315154413402 15772 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; config_reader.t100644000765000024 1160715154413402 15712 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use Test::Fatal; use Carp 'croak'; use Path::Tiny qw< path >; use Dancer2::Core::Runner; use Dancer2::ConfigReader; use Dancer2::ConfigReader::Config::Any; # 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 = path( __FILE__() )->sibling('config'); my $location2 = path( __FILE__() )->sibling('config2'); { package ConfigUser; use Moo; with 'Dancer2::Core::Role::HasConfig'; has environment => ( is => 'ro', required => 1 ); has location => ( is => 'ro', required => 1 ); has default_config => ( is => 'ro', required => 1 ); sub _build_config { my $self = shift; return Dancer2::ConfigReader->new( environment => $self->environment, location => $self->location, default_config => $self->default_config, )->config; } } sub config_any { my ( $environment, $location ) = @_; return Dancer2::ConfigReader::Config::Any->new( environment => $environment, location => $location, ); } sub config_reader { my ( $environment, $location ) = @_; return Dancer2::ConfigReader->new( environment => $environment, location => $location, default_config => $runner->config, ); } sub config_user { my ( $environment, $location ) = @_; return ConfigUser->new( environment => $environment, location => $location, default_config => $runner->config, ); } my $d = config_any( 'development', $location ); is_deeply $d->config_files, [ path( $location, 'config.yml' ), ], "config_files() only sees existing files"; my $f_any = config_any( 'production', $location ); is $f_any->does('Dancer2::Core::Role::ConfigReader'), 1, "role Dancer2::Core::Role::ConfigReader is consumed"; is_deeply $f_any->config_files, [ path( $location, 'config.yml' ), path( $location, 'environments', 'production.yml' ), ], "config_files() works"; my $j = config_any( 'staging', $location ); 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_any = config_any( 'failure', $location ); is $fail_any->environment, 'failure'; is_deeply $fail_any->config_files, [ path( $location, 'config.yml' ), path( $location, 'environments', 'failure.yml' ), ], "config_files() works"; like( exception { config_reader( 'failure', $location->stringify )->config }, qr{Unable to parse the configuration file}, 'Configuration file parsing failure', ); note "config merging"; my $m = config_reader( 'merging', $location->stringify ); # 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_any = config_any( 'lconfig', $location2->stringify ); is_deeply $l_any->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"; my $l = config_reader( 'lconfig', $location2->stringify ); 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"; my $f = config_user( 'production', $location->stringify ); 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 { config_reader( 'production', $location->stringify ) ->_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 = Path::Tiny->tempdir( CLEANUP => 1, TMPDIR => 1 ); $ENV{DANCER_CONFDIR} = $tmpdir; my $f = config_any( 'production', $location->stringify ); is $f->config_location, $tmpdir; } done_testing; strict_config.t100644000765000024 721415154413402 15737 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use Dancer2::ConfigReader; use Dancer2::FileUtils qw/dirname path/; use File::Spec; { package Dancer2::ConfigReader::TestWarn; use Moo; with 'Dancer2::Core::Role::ConfigReader'; has name => ( is => 'ro', default => sub { 'TestWarn' }, ); has config_data => ( is => 'ro', default => sub { {} }, ); sub read_config { my ($self) = @_; return $self->config_data; } } my $location = File::Spec->rel2abs( path( dirname(__FILE__), 'config' ) ); sub _read_config_with_warnings { my ($config_data) = @_; my $reader = Dancer2::ConfigReader::TestWarn->new( environment => 'test', location => $location, config_data => $config_data, ); my $cfgr = Dancer2::ConfigReader->new( environment => 'test', location => $location, default_config => {}, config_readers => [$reader], ); my @warnings; local $SIG{__WARN__} = sub { push @warnings, @_ }; $cfgr->config; return join q{}, @warnings; } subtest 'warns on unknown keys' => sub { my $warnings = _read_config_with_warnings({ strict_config => 1, typo => 1, engines => { logger => { File => { log_dir => '/tmp', log_level => 'debug', extra => 1, }, }, template => { template_toolkit => { foo => 1, }, }, serializer => { JSON => { allow_nonref => 1, foo => 2, }, }, }, }); like( $warnings, qr/Unknown configuration key 'typo'/, 'warns for unknown top-level key', ); like( $warnings, qr/Unknown configuration key 'extra' for engine 'logger\/File'/, 'warns for unknown engine key', ); unlike( $warnings, qr/template_toolkit/, 'does not warn for template_toolkit keys', ); unlike( $warnings, qr/serializer\/JSON/, 'does not warn for JSON serializer keys', ); }; subtest 'can disable warnings' => sub { my $warnings = _read_config_with_warnings({ strict_config => 0, typo => 1, engines => { logger => { File => { extra => 1, }, }, }, }); is( $warnings, q{}, 'warnings silenced' ); }; subtest 'can allow specific top-level keys' => sub { my $warnings = _read_config_with_warnings({ strict_config => 1, strict_config_allow => [ 'typo', 'extra_top_level' ], typo => 1, extra_top_level => 1, nope => 1, engines => { logger => { File => { extra => 1, }, }, }, }); unlike( $warnings, qr/Unknown configuration key 'typo'/, 'does not warn for allowlisted keys', ); unlike( $warnings, qr/Unknown configuration key 'extra_top_level'/, 'does not warn for allowlisted keys', ); like( $warnings, qr/Unknown configuration key 'nope'/, 'still warns for other top-level keys', ); like( $warnings, qr/Unknown configuration key 'extra' for engine 'logger\/File'/, 'still warns for unknown engine keys', ); }; done_testing; session_hooks.t100644000765000024 1665715154413402 16023 0ustar00jasonstaff000000000000Dancer2-2.1.0/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_stacktrace => 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; template_name.t100644000765000024 66315154413402 15676 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; 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; plugin_syntax.t100644000765000024 1001415154413402 16016 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; public000755000765000024 015154413402 14027 5ustar00jasonstaff000000000000Dancer2-2.1.0/tfile.txt100644000765000024 2615154413402 15605 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/publicthis is a public file plugin2000755000765000024 015154413402 14131 5ustar00jasonstaff000000000000Dancer2-2.1.0/thooks.t100644000765000024 340215154413402 15600 0ustar00jasonstaff000000000000Dancer2-2.1.0/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'; }; basic.t100644000765000024 120115154413402 15531 0ustar00jasonstaff000000000000Dancer2-2.1.0/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'; } tokens.tt100644000765000024 27315154413402 15704 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 %] issues000755000765000024 015154413402 14064 5ustar00jasonstaff000000000000Dancer2-2.1.0/tgh-811.t100644000765000024 245115154413402 15320 0ustar00jasonstaff000000000000Dancer2-2.1.0/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, "/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 "$url/get"; $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-596.t100644000765000024 66115154413402 15313 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' ); gh-762.t100644000765000024 176015154413402 15327 0ustar00jasonstaff000000000000Dancer2-2.1.0/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-936.t100644000765000024 122015154413402 15321 0ustar00jasonstaff000000000000Dancer2-2.1.0/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-723.t100644000765000024 225515154413402 15324 0ustar00jasonstaff000000000000Dancer2-2.1.0/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-794.t100644000765000024 76315154413402 15316 0ustar00jasonstaff000000000000Dancer2-2.1.0/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, 400, 'Failed to decode invalid content', ); gh-931.t100644000765000024 307615154413402 15327 0ustar00jasonstaff000000000000Dancer2-2.1.0/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-797.t100644000765000024 237615154413402 15343 0ustar00jasonstaff000000000000Dancer2-2.1.0/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-634.t100644000765000024 500315154413402 15317 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/issuesuse strict; use warnings; use Test::More tests=> 3; use File::Temp qw/tempdir/; use Path::Tiny qw< path >; my $log_dir = tempdir( CLEANUP => 1 ); sub config_location { my ($app) = @_; my %config_readers = map { $_->{name} => $_ } @{ $app->config_reader->config_readers }; return $config_readers{ 'Config::Any' }->config_location; } { 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, config_location( $app ), '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, path( $dir, $file )->stringify, '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, path( config_location( $app ), 'logs' )->stringify, $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-799.t100644000765000024 313415154413402 15336 0ustar00jasonstaff000000000000Dancer2-2.1.0/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-944.t100644000765000024 202115154413402 15320 0ustar00jasonstaff000000000000Dancer2-2.1.0/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-730.t100644000765000024 255315154413402 15323 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' ); }; send_file.t100644000765000024 1162515154413402 15635 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/dsluse strict; use warnings; use utf8; use Encode 'encode_utf8'; use Test::More; use Plack::Test; use HTTP::Request::Common; use Path::Tiny (); 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'; }; get '/illegal' => 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 $file = Path::Tiny::path(__FILE__)->absolute->stringify; send_file($file, content_type => 'image/png', system_path => 1); }; get '/no_streaming' => sub { my $file = Path::Tiny::path(__FILE__)->absolute->stringify; send_file( $file, system_path => 1, streaming => 0 ); }; get '/options_streaming' => sub { my $file = Path::Tiny::path(__FILE__)->absolute->stringify; send_file( $file, system_path => 1, streaming => 1 ); }; get '/content_disposition/attachment' => sub { send_file('1x1.png', filename => '1x1.png'); }; get '/content_disposition/inline' => sub { send_file('1x1.png', filename => '1x1.png', content_disposition => 'inline'); }; } 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 default 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'); }; subtest 'Content-Disposition defaults to "attachment"' => sub { my $r = $cb->( GET '/content_disposition/attachment' ); ok($r->is_success, 'send_file returns success'); is($r->header('Content-Disposition'), 'attachment; filename="1x1.png"', 'send_file returns correct attachment Content-Disposition'); }; subtest 'Content-Disposition supports "inline"' => sub { my $r = $cb->( GET '/content_disposition/inline' ); ok($r->is_success, 'send_file returns success'); is($r->header('Content-Disposition'), 'inline; filename="1x1.png"', 'send_file returns correct inline Content-Disposition'); }; subtest "Illegal path" => sub { my $r = $cb->( GET '/illegal' ); is( $r->code, 403, 'Illegal path returns 403' ); is( $r->content, 'Forbidden', 'Text content contains UTF-8 characters', ); }; }; done_testing; CODE_OF_CONDUCT.md100644000765000024 655215154413402 15255 0ustar00jasonstaff000000000000Dancer2-2.1.0# Overview This document describes various policies (most notably, the standards of conduct) for the Dancer core developers and broad community. This is what we expect from our community and ourselves and these are the standards of behavior we set forth in order to make sure the community remains a safe space for all of its members, without exception. # Standards of Conduct These standards apply anywhere the community comes together as a group. This includes, but is not limited to, the Dancer IRC channel, the Dancer mailing list, Dancer hackathons, and Dancer conferences. - Always be civil. - Heed the moderators. - Abuse is not tolerated. Civility is simple: stick to the facts while avoiding demeaning remarks and sarcasm. It is not enough to be factual. You must also be civil. Responding in kind to incivility is not acceptable. If the list moderators tell you that you are not being civil, carefully consider how your words have appeared before responding in any way. You may protest, but repeated protest in the face of a repeatedly reaffirmed decision is not acceptable. Unacceptable behavior will result in a public and clearly identified warning. Repeated unacceptable behavior will result in removal from the mailing list and revocation of any commit bit. The first removal is for one month. Subsequent removals will double in length. After six months with no warning, a user's ban length is reset. Removals, like warnings, are public. The list of moderators consists of all active core developers. This includes, in alphabetical order, Alberto Simões, David Precious, Jason Crome, Mickey Nasriachi, Peter Mottram, Russell Jenkins, Sawyer X, Stefan Hornburg (Racke), and Yanick Champoux. This list might additionally grow to active members of the community who have stepped up to help handle abusive behavior. If this should happen, this document would be updated to include their names. Additionally, it's important to understand the self-regulating nature we foster at the Dancer community. This means anyone and everyone in the community - in the channel, on the list, at an event - has the ability to call out unacceptable behavior and incivility to others in the community. Moderators are responsible for issuing warnings and take disciplinary actions, but anyone may - and is encouraged - to publicly make note of unacceptable treatment of others. As a core principle, abuse is never tolerated. One cannot berate, insult, debase, deride, put down, or vilify anyone, or act towards anyone in a way intending to hurt them. The community specifically considers as abuse any attempts to otherize anyone by any individual characteristic, including, but not limited to, their technical skill, knowledge or by their age, colour, disability, gender, language, national or social origin, political or other opinion, race, religion, sex, or sexual orientation. The community aims to maintain a safe space for everyone, in any forum it has. If you ever feel this core principle has been compromised, you are strongly urged to contact a moderator. We are always here. Remember, this is **your** community, as much as it is anyone else's. # CREDITS This policy has been adopted and adapted from the policy available for the Perl language development, provided by **p5p** (the Perl 5 Porters). The original inspiration policy document can be read at [perlpolicy](https://metacpan.org/pod/perlpolicy). charset_server.t100644000765000024 241215154413402 16114 0ustar00jasonstaff000000000000Dancer2-2.1.0/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(); logger_console.t100644000765000024 264415154413402 16105 0ustar00jasonstaff000000000000Dancer2-2.1.0/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 request_upload.t100644000765000024 1537215154413402 16162 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings FATAL => 'all'; use Test::More; use Test::Fatal; use Dancer2::Core::Request; use Carp; use File::Temp 0.22; use Path::Tiny qw< path >; 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 path($file)->parent, $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 and decoded filename"; my $encoded_filename = encode_utf8($filename); is_deeply $uploads[0]->headers, { 'Content-Disposition' => qq[form-data; name="test_upload_file"; filename="$encoded_filename"], 'Content-Type' => 'text/plain', }; is $uploads[0]->filename, $filename; 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 = path( $dest_dir, $upload->basename )->stringify; $upload->copy_to($dest_file); ok( ( -f $dest_file ), "file '$dest_file' has been copied" ); my $dest_file_link = path( $dest_dir, "hardlink" )->stringify; $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 request object lives' ); # On Windows, files cannot be unlinked while open. Close all cached # file handles before destroying $req so that the temp dir cleanup # (which runs in File::Temp::Dir's DESTROY, triggered by freeing # $req's PSGI env) can succeed. if ( $^O eq 'MSWin32' ) { for my $up_or_list ( values %{ $req->uploads } ) { my @ups = ref $up_or_list eq 'ARRAY' ? @{$up_or_list} : $up_or_list; $_->{_fh} = undef for @ups; } } undef $req; SKIP: { skip "Win32 can't remove file/link while open due to deadlock", 1 if ( $^O eq 'MSWin32' ); ok( ( !-f $file ), 'temp file is removed when request 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::file_handle"}{CODE}; no warnings 'redefine'; *{"Dancer2::Core::Request::Upload::file_handle"} = sub { croak "Can't open mocked-tempfile using mode '<'"; }; } $upload->{_fh} = undef; like( exception { $upload->file_handle }, qr{Can't open mocked-tempfile using mode '<'}, ); # unmock open_file { no strict 'refs'; no warnings 'redefine'; *{"Dancer2::Core::Request::Upload::file_handle"} = $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; session_object.t100644000765000024 217715154413402 16116 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; shared_engines.t100644000765000024 212215154413402 16051 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; static_content.t100644000765000024 115515154413402 16121 0ustar00jasonstaff000000000000Dancer2-2.1.0/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(); session_config.t100644000765000024 421315154413402 16106 0ustar00jasonstaff000000000000Dancer2-2.1.0/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', cookie_same_site => 'Strict', 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, 10 ]; } ); 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"; is $opts->{SameSite}, 'Strict', "cookie has same site set to strict"; # 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; gh-1098.t100644000765000024 513715154413402 15414 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/issuesuse Test::More tests => 3; use Test::Fatal; use Dancer2::Core::Error; use Dancer2::Core::Response; use Dancer2::Serializer::JSON; use HTTP::Headers; 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_stacktrace => []) }, qr/Reference \Q[]\E did not pass type constraint "Bool"/i, "Error->new(show_stacktrace => []) 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-1232.t100644000765000024 236315154413402 15400 0ustar00jasonstaff000000000000Dancer2-2.1.0/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();gh-1070.t100644000765000024 75015154413402 15356 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/issuesuse strict; use warnings; use Test::More tests => 2; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; set show_stacktrace => 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-1564.t100644000765000024 651415154413402 15412 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/issues#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 2; use Plack::Test; use Plack::Builder; use HTTP::Request::Common; { package App; use Dancer2; get '/root' => sub { redirect '/'; }; get '/leading_slash' => sub { redirect '/expected'; }; get '/relative' => sub { redirect 'expected'; }; get '/relative/one-dot' => sub { redirect './expected'; }; get '/relative/two-dots' => sub { redirect '../expected'; }; get '/absolute' => sub { redirect 'http://expected' }; get '/schemeless' => sub { redirect '//expected' }; } my $app = builder { mount '/' => App->to_app(); mount '/other-mount-point' => App->to_app(); }; my @leading_slash_tests = ( # the expected result (last element) needs the mount prepended in the actual tests [ 'Relative with leading slash', '/leading_slash', '/expected' ], [ 'Relative root', '/root', '/' ], # Test for issue #1611 ); my @common_tests = ( [ 'Relative redirect', '/relative', 'expected' ], [ 'Relative redirect with ./', '/relative/one-dot', './expected' ], [ 'Relative redirect with ../', '/relative/two-dots', '../expected' ], [ 'Absolute URL redirect', '/absolute', 'http://expected' ], [ 'Schemeless redirect', '/schemeless', '//expected' ], ); subtest 'Testing app mounted to /' => sub { my $mount = ''; test_psgi $app, sub { my $cb = shift; for my $test (@leading_slash_tests) { my ($name, $url, $expected) = @$test; subtest $name => sub { my $res = $cb->( GET $url ); is($res->code, 302, 'Correct code'); is( $res->headers->header('Location'), $mount . $expected, 'Correct location header' ); } } foreach my $test (@common_tests) { my ($name, $url, $expected) = @$test; subtest $name => sub { my $res = $cb->( GET $url ); is($res->code, 302, 'Correct code'); is( $res->headers->header('Location'), $expected, 'Correct location header' ); } } }; }; subtest 'Testing app mounted to /other-mount-point' => sub { my $mount = '/other-mount-point'; test_psgi $app, sub { my $cb = shift; for my $test (@leading_slash_tests) { my ($name, $url, $expected) = @$test; $url = "$mount$url"; subtest $name => sub { my $res = $cb->( GET $url ); is($res->code, 302, 'Correct code'); is( $res->headers->header('Location'), "$mount$expected", # scriptname prepended to redirect path 'Correct location header' ); } } foreach my $test (@common_tests) { my ($name, $url, $expected) = @$test; $url = "$mount$url"; subtest $name => sub { my $res = $cb->( GET $url ); is($res->code, 302, 'Correct code'); is( $res->headers->header('Location'), $expected, 'Correct location header' ); } } }; }; note 'DONE!'; gh-1289.t100644000765000024 137015154413402 15411 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/issuesuse strict; use warnings; use Test::More tests => 1; use Plack::Test; use HTTP::Request::Common; { package App; use Dancer2; post '/forwards' => sub { forward('/still_contains_uploads'); }; any '/still_contains_uploads' => sub { my $upload = request->upload('testupload'); if ($upload) { return; } die 'failed'; }; } { my $test = Plack::Test->create(App->to_app); my $response = $test->request( POST( '/forwards', Content_Type => 'form-data', Content => [testupload => [undef, 'testupload', Content => "testcontent"]] ) ); is($response->code, 200, 'Uploads survive forward'); } done_testing(); parameters.t100644000765000024 2377315154413402 16057 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' ); } }; 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(); Dancer2000755000765000024 015154413402 14332 5ustar00jasonstaff000000000000Dancer2-2.1.0/libCLI.pm100644000765000024 304715154413402 15443 0ustar00jasonstaff000000000000Dancer2-2.1.0/lib/Dancer2package Dancer2::CLI; # ABSTRACT: Dancer2 CLI application $Dancer2::CLI::VERSION = '2.1.0'; use Moo; use CLI::Osprey; use Path::Tiny; use File::Share 'dist_dir'; use Module::Runtime 'use_module'; subcommand gen => 'Dancer2::CLI::Gen'; subcommand 'gen-app' => 'Dancer2::CLI::Gen'; # Could have done this one inline, but wanted to remain consistent # across subcommands. subcommand version => 'Dancer2::CLI::Version'; # Thinking ahead, these might be useful in future subcommands has _dancer2_version => ( is => 'lazy', builder => sub { use_module( 'Dancer2' )->VERSION }, ); has _dist_dir => ( is => 'lazy', builder => sub{ dist_dir('Dancer2') }, ); sub run { my $self = shift; return $self->osprey_usage; } sub _get_app_path { my ( $self, $path, $appname ) = @_; return path( $path, $self->_get_dashed_name( $appname )); } sub _get_app_file { my ( $self, $appname ) = @_; $appname =~ s{::}{/}g; return path( 'lib', "$appname.pm" ); } sub _get_perl_interpreter { return -r '/usr/bin/env' ? '#!/usr/bin/env perl' : "#!$^X"; } sub _get_dashed_name { my ( $self, $name ) = @_; $name =~ s{::}{-}g; return $name; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::CLI - Dancer2 CLI application =head1 VERSION version 2.1.0 =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 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 author-no-tabs.t100644000765000024 3046415154413402 15770 0ustar00jasonstaff000000000000Dancer2-2.1.0/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/Gen.pm', 'lib/Dancer2/CLI/Version.pm', 'lib/Dancer2/ConfigReader.pm', 'lib/Dancer2/ConfigReader/Config/Any.pm', 'lib/Dancer2/ConfigUtils.pm', '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/HasConfig.pm', 'lib/Dancer2/Core/Role/HasEnvironment.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/DeprecationPolicy.pod', '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/Config.pod', 'lib/Dancer2/Manual/Cookbook.pod', 'lib/Dancer2/Manual/Deployment.pod', 'lib/Dancer2/Manual/Extending.pod', 'lib/Dancer2/Manual/Keywords.pod', 'lib/Dancer2/Manual/Migration.pod', 'lib/Dancer2/Manual/Plugins.pod', 'lib/Dancer2/Manual/QuickStart.pod', 'lib/Dancer2/Manual/Testing.pod', 'lib/Dancer2/Manual/Tutorial.pod', 'lib/Dancer2/Plugin.pm', '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/TemplateToolkit.pm', 'lib/Dancer2/Template/Tiny.pm', 'lib/Dancer2/Test.pm', '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/t3/lib/App4.pm', 't/app/t3/lib/config.yml', 't/app/t_config_file_extended/bin/app.psgi', 't/app/t_config_file_extended/config.yml', 't/app/t_config_file_extended/lib/App1.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-HasConfig/with.t', 't/classes/Dancer2-Core-Role-HasEnvironment/with.t', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists', 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/blib/lib/fakescript.pl', '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.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_file_extended.t', 't/config_many.t', 't/config_many_failure.t', 't/config_multiapp.t', 't/config_reader.t', 't/config_settings.t', 't/config_utils.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/empty.foo', '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/mime.t', 't/dsl/parameters.t', 't/dsl/pass.t', 't/dsl/path.t', 't/dsl/pod.t', 't/dsl/request.t', 't/dsl/request_data.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/dsl/uri_for.t', 't/dsl/uri_for_route.t', 't/dsl/yaml.t', 't/engine.t', 't/error.t', 't/examples/hello_world.t', 't/examples/simple_calculator.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-1289.t', 't/issues/gh-1449/TestPlugin.pm', 't/issues/gh-1449/gh-1449.t', 't/issues/gh-1564.t', 't/issues/gh-1621/gh-1621.t', 't/issues/gh-1621/views/redirect.tt', 't/issues/gh-1664/gh-1664.t', 't/issues/gh-1712/gh-1712.t', 't/issues/gh-1712/views/exec.tt', '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/ConfigReader/Additional.pm', 't/lib/Dancer2/ConfigReader/File/Extended.pm', 't/lib/Dancer2/ConfigReader/Recursive.pm', 't/lib/Dancer2/ConfigReader/TestDummy.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/Dancer2/Template/TemplateToolkitFoo.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/TemplateFoo.pm', 't/lib/TestApp.pm', 't/lib/TestPod.pm', 't/lib/TestTypeLibrary.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/multipart_content.t', 't/named_apps.t', 't/named_routes.t', 't/no_default_middleware.t', 't/plugin2/app_dsl_cb/app_dsl_cb.t', 't/plugin2/app_dsl_cb/lib/App.pm', 't/plugin2/app_dsl_cb/lib/App/TestPlugin.pm', '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_multipart_formdata.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/serializer_mutable_custom.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/session_yaml_object.t', 't/shared_engines.t', 't/static_content.t', 't/strict_config.t', 't/template.t', 't/template_default_tokens.t', 't/template_ext.t', 't/template_name.t', 't/time.t', 't/types.t', 't/utf8_strict.t', 't/utf8_url.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; plugin_register.t100644000765000024 231015154413402 16274 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.t100644000765000024 1157215154413402 16333 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; serializer_json.t100644000765000024 317215154413402 16303 0ustar00jasonstaff000000000000Dancer2-2.1.0/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(); session_engines.t100644000765000024 617315154413402 16300 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use YAML; use Plack::Test; use HTTP::Cookies; use HTTP::Request::Common; use Path::Tiny qw< path >; my $SESSION_DIR; BEGIN { $SESSION_DIR = path( __FILE__ )->parent->child('sessions')->stringify; } { 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_stacktrace => 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; config_multiapp.t100644000765000024 63715154413402 16244 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; 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 $app->config->{app}->{config}, 'ok', $app->name . ": config loaded properly" } done_testing; config_settings.t100644000765000024 132215154413402 16261 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; config.yml100644000765000024 4715154413402 15747 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t2app: config: ok strict_config: 0 t1000755000765000024 015154413402 13655 5ustar00jasonstaff000000000000Dancer2-2.1.0/t/appconfig.yml100644000765000024 4715154413402 15746 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t1app: config: ok strict_config: 0 config000755000765000024 015154413402 14016 5ustar00jasonstaff000000000000Dancer2-2.1.0/tconfig.yml100644000765000024 14015154413402 16121 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/configmain: 1 charset: 'UTF-8' show_stacktrace: 1 strict_config: 0 application: some_feature: foo basic-2.t100644000765000024 133115154413402 15674 0ustar00jasonstaff000000000000Dancer2-2.1.0/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/; } config.yml100644000765000024 1715154413402 16152 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/issueslogger: "Note" Test.pm100644000765000024 6673515154413402 16010 0ustar00jasonstaff000000000000Dancer2-2.1.0/lib/Dancer2package Dancer2::Test; # ABSTRACT: Useful routines for testing Dancer2 apps $Dancer2::Test::VERSION = '2.1.0'; 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::MIME; 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 { croak 'DEPRECATED: Dancer2::Test. 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], mime_type => Dancer2::Core::MIME->new(), ); } 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 'DEPRECATED: Dancer2::Test. 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 2.1.0 =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 The routines provided by this module for testing Dancer2 apps are buggy and unnecessary. Instead, use the L module as shown in the SYNOPSIS above and ignore the functions in this documentation. Consult the L documentation for further details. This module will be removed from the Dancer2 distribution in the near future. You should migrate all tests that use it over to the L module and remove this module from your system. This module will throw warnings to remind you. For now, you can silence the warnings by setting the C option: $Dancer::Test::NO_WARN = 1; In the functions below, $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) 2026 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 Core.pm100644000765000024 140415154413402 15717 0ustar00jasonstaff000000000000Dancer2-2.1.0/lib/Dancer2package Dancer2::Core; # ABSTRACT: Core libraries for Dancer2 2.0 $Dancer2::Core::VERSION = '2.1.0'; 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 2.1.0 =head1 FUNCTIONS =head2 camelize Camelize a underscore-separated-string. =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 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 author-distmeta.t100644000765000024 42415154413402 16170 0ustar00jasonstaff000000000000Dancer2-2.1.0/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::MetaTests use strict; use warnings; use Test::CPAN::Meta; meta_yaml_ok(); Releasing-Dancer2.md100644000765000024 475515154413402 16170 0ustar00jasonstaff000000000000Dancer2-2.1.0# Releasing Dancer2 This document is an overview of the process we go through when putting together a new release of Dancer2. This is a guide, not a bible - we don't follow this religiously - but it is better to do more of what's in here than less. ## Ongoing For all PRs and branches merged: - Make sure all tests pass on the branch to be merged. - Make sure Changes is updated. Include the GitHub issue or PR # in the changelog entry. - Make sure the author is reflected in the Contributors section of Dancer2.pm - `git merge --no-ff && git push` ## One week before release Notify the Core Team that a release is going out. Do this via the dancer-dev mailing list and Twist. If anyone has something to be merged, now is the time to bring it up. If the release needs to be delayed in order for someone to get a change into a release, this is the time to pump the brakes. ## Day of Release ### Pull the latest code from main Don't be That Person. Make sure everything you have is current. ### Ensure all changes are merged Each release should have a milestone in GitHub. Make sure all issues and PRs in the milestone are closed. If an issue or PR needs to move to a later release, do that now. ### Update the version in `dist.ini` We use [Semantic Versioning](https://semver.org/) for all Dancer2 releases. ### Do the release! Run `dzil release --all`. This will: - Run the test suite, including the author tests - Build a tarball for upload to PAUSE - Upload the new version to PAUSE - Commit `Dancer2.pm` and the new `README` file to the repo (after which, you need to `git push`) After this, you must `git push` to push the updated release files to GitHub. The test suite *must* pass before a release can be done. This is your last chance to resolve any test failures. Provided the tests pass, you will be prompted to continue the release process. Provided you have your PAUSE credentials set up, this will upload the new dist to PAUSE. If you don't have a credential file set up, you can manually update the new release via the PAUSE web interface. ### Send out release announcements Historically, release announcements have been sent out via the following channels: - dancer-users mailing list - Twitter/X - blogs.perl.org Going forward, we intend to also send out announcements via: - LinkedIn - Perl Community group on Facebook - dev.to - Medium Reshare from any accounts you have. Blog about it. ## After the Release Watch for bug reports, praise, criticism, and respond appropriately. forward_test_tcp.t100644000765000024 357715154413402 16463 0ustar00jasonstaff000000000000Dancer2-2.1.0/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'; $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'; }; done_testing(); config2000755000765000024 015154413402 14100 5ustar00jasonstaff000000000000Dancer2-2.1.0/tconfig.yml100644000765000024 22415154413402 16206 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/config2main: 1 charset: 'UTF-8' show_stacktrace: 1 strict_config: 0 application: feature_1: foo feature_2: bar feature_3: baz feature_4: blat lib000755000765000024 015154413402 14424 5ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t2App3.pm100644000765000024 7015154413402 15662 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t2/libpackage App3; use strict; use warnings; use Dancer2; 1; lib000755000765000024 015154413402 14425 5ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t3App4.pm100644000765000024 7215154413402 15666 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t3/libpackage App4; use strict; use warnings; use Dancer2; 1; lib000755000765000024 015154413402 14423 5ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t1App1.pm100644000765000024 7115154413402 15660 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t1/libpackage App1; use strict; use warnings; use Dancer2; 1; MyDancerDSL.pm100644000765000024 163315154413402 16065 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/libpackage MyDancerDSL; use Moo; 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; TemplateFoo.pm100644000765000024 31515154413402 16213 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/libpackage TemplateFoo; # Custom Template::Toolkit class use base 'Template'; sub process { my ($self, $template, $vars, $outstream, @opts) = @_; $$outstream = "Custom Render Template"; 1; } 1; auto_page.tt100644000765000024 4115154413402 16316 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/viewsHey! This is Auto Page wörking. request_data.t100644000765000024 240015154413402 16335 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; subtest 'request data basic' => sub { { package App::Body::Str; ## no critic use Dancer2; post '/' => sub { my $data = request_data; ::is( $data, 'a string body', 'string content ok' ); }; } my $app = Plack::Test->create( App::Body::Str->to_app ); my $res = $app->request( POST '/', Content_Type => 'text/plain', Content => "a string body" ); ok( $res->is_success, 'Successful request' ); }; subtest 'request data serialized' => sub { { package App::Body::JSON; ## no critic use Dancer2; set serializer => 'JSON'; post '/' => sub { my $data = request_data; ::is_deeply( $data, { body => { is => [ "json" ] } }, 'json content ok' ); return +{ ok => 1 }; }; } my $app = Plack::Test->create( App::Body::JSON->to_app ); my $res = $app->request( POST '/', Content_Type => 'application/json', Content => '{"body":{"is":["json"]}}' ); ok( $res->is_success, 'Successful request' ); }; done_testing(); context-in-before.t100644000765000024 211715154413402 16427 0ustar00jasonstaff000000000000Dancer2-2.1.0/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' ); }; multipart_content.t100644000765000024 216215154413402 16652 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package App; use Dancer2; get '/' => sub {1}; } # # Test for this issue: https://github.com/PerlDancer/Dancer2/issues/1507 # When a request comes with Content-Type: multipart/form-data with no boundary, # Dancer currently wrongly returns HTTP code 500 Internal Server Error. # It should return HTTP code 400 Bad Request. # We also test that a request with Content-Type: multipart/form-data boundary=------boundary-------' returns 200. my $app = App->to_app; ok( is_coderef($app), 'Got app' ); test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/' => 'Content-Type' => 'multipart/form-data' )->code, 400, 'multipart with incorrect boundary returns 400', ); my $headers = is( $cb->( GET '/' => 'Content-Type' => 'Content-Type: multipart/form-data boundary=------boundary-------', )->code, 200, 'Providing multipart with correct boundary works', ); }; done_testing(); session_lifecycle.t100644000765000024 1461215154413402 16624 0ustar00jasonstaff000000000000Dancer2-2.1.0/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_stacktrace => 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; bin000755000765000024 015154413402 14425 5ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t1app.psgi100644000765000024 4715154413402 16172 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/app/t1/bin#!perl use Dancer2; use App1; start; no-config.t100644000765000024 77315154413402 16324 0ustar00jasonstaff000000000000Dancer2-2.1.0/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()"; uri_for_route.t100644000765000024 1563715154413402 16577 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/dsluse strict; use warnings; use Test::More 'tests' => 3; use Plack::Test; use Plack::Builder; use HTTP::Request::Common; use JSON::MaybeXS; { package App; use Dancer2; our $tested; # Static with route params # Static with code # Static with options and code get 'view_entry_static1' => '/view1/:id' => sub {1}; get 'view_entry_static2' => '/view2/:id' => { 'user_agent' => 'UA/1.0' }, sub {1}; # static with typed route param get 'view_user' => '/:prefix/user/:username[Str]' => sub {1}; # splat / megasplat get 'view_entry_splat' => '/viewsplat/*/*/**' => sub {1}; # Mixed with splat/megasplat # Different method patch 'view_entry_mixed' => '/view_mixed/*/**/:id' => sub {1}; # Regexp - fails get 'view_entry_regexp1' => qr{/rview1/[0-9]+} => sub {1}; post '/uri_for_route' => sub { my $params = JSON::MaybeXS::decode_json( request->content ); return uri_for_route( $params->{'route_name'}, $params->{'route_params'}, $params->{'query_params'} // {}, !!$params->{'dont_escape'}, ); }; get '/fail_uri_for_route' => sub { my $failed = 0; eval { uri_for_route('vvv'); 1; } or do { ::like( $@, qr/\QCannot find route named 'vvv'\E/xms, 'Cannot retrieve nonexistent route', ); $failed++; }; return $failed; }; get '/fail_uri_for_route_splat_args' => sub { my $failed = 0; eval { uri_for_route( 'view_entry_splat', ['foo'], ); 1; } or do { ::like( $@, qr/\QMismatch in amount of splat args and splat elements\E/xms, 'Cannot handle mismatched splat args and elements', ); $failed++; }; return $failed; }; get '/fail_uri_for_route_leftovers' => sub { my $failed = 0; eval { uri_for_route('view_entry_static1'); 1; } or do { my $msg = 'Route view_entry_static1 uses the parameter \'id\', ' . 'which was not provided'; ::like( $@, qr/\Q$msg\E/xms, 'Cannot handle leftover route parameters', ); $failed++; }; return $failed; }; # Error defining two routes with the same name, regardless of method eval { get 'view_entry_splat' => '/' => sub {1}; 1; } or do { ::like( $@, qr/\QRoute with this name (view_entry_splat) already exists\E/xms, 'Cannot register two routes with same name', ); $tested = 1; }; } sub test_app { my ( $app, $mount_path ) = @_; my $prefix = 'http://localhost'; $mount_path and $prefix .= $mount_path; my ( $path, $res ); # Test static paths foreach my $idx ( 1 .. 2 ) { $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json({ 'route_name' => "view_entry_static$idx", 'route_params' => { 'id' => $idx }, 'query_params' => { 'foo' => $idx }, }), ) ); $path = "$prefix/view$idx/$idx?foo=$idx"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path: $path" ); } # Test splat + megasplat $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json({ 'route_name' => 'view_entry_splat', 'route_params' => [ 'foo', 'bar', [ 'baz', 'quux' ] ], 'query_params' => { 'id' => 'di' }, }), ) ); $path = "$prefix/viewsplat/foo/bar/baz/quux?id=di"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path: $path" ); # Test mixed $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json( { 'route_name' => 'view_entry_mixed', 'route_params' => { 'id' => 'di', 'splat' => ['foo', ['bar', 'baz']] }, 'query_params' => {'foo' => 'bar'}, } ), ) ); $path = "$prefix/view_mixed/foo/bar/baz/di?foo=bar"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path: $path" ); # Test escaping $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json({ 'route_name' => 'view_entry_static1', 'route_params' => { 'id' => '!@£$%' }, }), ) ); $path = "$prefix/view1/!@%C3%82%C2%A3\$%"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path: $path" ); # Test nonexistent route name $res = $app->request( GET "$prefix/fail_uri_for_route" ); ok( $res->is_success, 'Successful request' ); is( $res->content, '1', 'Successfully tested nonexistent failure mode' ); # Test splat + megasplat (incorrect amount) $res = $app->request( GET "$prefix/fail_uri_for_route_splat_args" ); ok( $res->is_success, 'Successful request' ); is( $res->content, '1', 'Successfully tested mismatch splat args/elements failure mode' ); # Test mixed with not all filled (named args left) $res = $app->request( GET "$prefix/fail_uri_for_route_leftovers" ); ok( $res->is_success, 'Successful request' ); is( $res->content, '1', 'Successfully tested leftover args failure mode' ); # Static with typed route parameters $res = $app->request( POST( "$prefix/uri_for_route", 'Content' => JSON::MaybeXS::encode_json({ 'route_name' => 'view_user', 'route_params' => { 'prefix' => 'foo', 'username' => 'sawyer' }, 'query_params' => { 'foo' => 1 }, }), ) ); $path = "$prefix/foo/user/sawyer?foo=1"; ok( $res->is_success, 'Successful request' ); is( $res->content, $path, "Correct path for typed route param: $path" ); } subtest 'Non-mounted app' => sub { my $app = Plack::Test->create( App->to_app ); test_app($app); ok( $App::tested, 'Check for duplicate route names done successfully' ); }; subtest 'Mounted app' => sub { my $app = Plack::Test->create( builder { mount '/mount' => App->to_app; mount '/' => sub { return { Plack::Response->new(200, [], ['OK'] ) } }, } ); test_app( $app, '/mount' ); }; route_retvals.t100644000765000024 63115154413402 16536 0ustar00jasonstaff000000000000Dancer2-2.1.0/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; Plugin.pm100644000765000024 7636115154413402 16323 0ustar00jasonstaff000000000000Dancer2-2.1.0/lib/Dancer2package Dancer2::Plugin; # ABSTRACT: base class for Dancer2 plugins $Dancer2::Plugin::VERSION = '2.1.0'; 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, \$name, \%args ) = \@_; if (ref \$name eq 'ARRAY' && exists \$args{'plugin_keyword'} && ref \$args{'plugin_keyword'} eq 'ARRAY') { Carp::croak('Setting "plugin_keyword" to an array is disallowed' . ' when defining multiple attributes simultaneously'); } \$orig->( ${caller}->_p2_has( \$_, \%args) ) for ref \$name ? @\$name : \$name; }; 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 { Carp::croak "DEPRECATED: Plugin DSL method 'plugin_setting'. " . "Please use '\\\$self->config' instead\n" }; 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::croak "DEPRECATED: Plugin DSL method 'dancer_app'. " . "Please use '\\\$self->app' instead'.\n"; } # FIXME: AUTOLOAD might pick up on this sub request { Carp::croak "DEPRECATED: Plugin DSL method 'request'. " . "Please use '\\\$self->app->request' instead'.\n"; } # FIXME: AUTOLOAD might pick up on this sub var { Carp::croak "DEPRECATED: Plugin DSL method 'var'. " . "Please use '\\\$self->app->request->var' instead'.\n"; } # FIXME: AUTOLOAD might pick up on this sub hook { Carp::croak "DEPRECATED: Plugin DSL method 'hook'. " . "Please use '\\\$self->app->add_hook' instead'.\n"; } } END die $@ if $@; { ## no critic qw(TestingAndDebugging::ProhibitNoWarnings) no strict 'refs'; no warnings 'redefine'; *{"${caller}::dsl"} = sub { my $app_dsl_cb = _find_consumer($caller); return $app_dsl_cb ? $app_dsl_cb->() : undef; }; } return map { [ $_ => { class => $caller } ] } qw/ plugin_keywords plugin_hooks /; } sub _find_consumer { my %skip = map +( $_ => 1 ), @_; my $class; ## no critic qw(ControlStructures::ProhibitCStyleForLoops) for ( my $i = 1; my $caller = caller($i); $i++ ) { next if $skip{$caller}; next if eval { $caller->isa('Dancer2::Plugin') }; $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($plugin_module); # 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 { $_[0] ? do { my $cb = shift()->app->name->can($keyword); $cb->(@_); } : $app_dsl_cb->(@_); }; } }); } 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 2.1.0 =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'; =head3 Coverage for Dancer2::Plugin:: is 0.0%, with 15 naked subroutines To avoid errors caused by missing pod for private plugin methods, write your pod coverage test like so: pod_coverage_ok( "Dancer2::Plugin::MyAwesomePlugin", { also_private => [ qw/ BUILDARGS BUILD ClassHooks PluginKeyword dancer_app execute_plugin_hook hook keywords on_plugin_import plugin_args plugin_setting realms realm realm_providers register register_hook register_plugin request var / ] }); =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 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 author-pod-syntax.t100644000765000024 45315154413402 16466 0ustar00jasonstaff000000000000Dancer2-2.1.0/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.t100644000765000024 1360115154413402 16326 0ustar00jasonstaff000000000000Dancer2-2.1.0/t#!perl use strict; use warnings; # This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs 0.029 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 grep { $_ eq $mod } @exclude; my $want = $req_hash->{$phase}{$type}{$mod}; $want = "undef" unless defined $want; $want = "any" if !$want && $want == 0; if ($mod eq 'perl') { push @reports, ['perl', $want, $]]; next; } my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required"; my $file = $mod; $file =~ s{::}{/}g; $file .= ".pm"; my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC; 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('Reported prereqs'); # vim: ts=4 sts=4 sw=4 et: disp_named_capture.t100644000765000024 117515154413402 16730 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse warnings; use strict; use Plack::Test; use HTTP::Request; use Test::More tests => 2; { package app; use Dancer2; get '/1' => sub { return '1'; }; get '/2' => sub { return '2'; }; } my $test = Plack::Test->create( app->to_app ); my $request = HTTP::Request->new( GET => 'http://localhost/1' ); my $response = $test->request( $request ); is( $response->content, 1 ); # "Dummy" regex to populate global $+ "12345" =~ m#(?23)#; my $c = $+{capture}; $request = HTTP::Request->new( GET => 'http://localhost/2' ); $response = $test->request( $request ); is( $response->content, 2 ); multi_apps_forward.t100644000765000024 332615154413402 17003 0ustar00jasonstaff000000000000Dancer2-2.1.0/t#!perl use strict; use warnings; use Test::More tests => 9; use Plack::Test; use HTTP::Request::Common; { package App1; use Dancer2; get '/' => sub {'App1'}; get '/forward' => sub { forward '/'; ::ok( 0, 'Foward not returning right away!' ); }; get '/forward_to_new' => sub { forward '/new'; ::ok( 0, 'Foward not returning right away!' ); }; } { package App2; use Dancer2; get '/' => sub {'App2'}; get '/new' => sub {'New'}; } { # test each single app my $app1 = App1->to_app; test_psgi $app1, sub { my $cb = shift; is( $cb->( GET '/' )->code, 200, '[GET /] OK' ); is( $cb->( GET '/' )->content, 'App1', '[GET /] OK content' ); is( $cb->( GET '/forward' )->code, 200, '[GET /forward] OK' ); is( $cb->( GET '/forward' )->content, 'App1', '[GET /forward] OK content' ); is( $cb->( GET '/forward_to_new' )->code, 404, 'Cannot find /new', ); }; my $app2 = App2->to_app; test_psgi $app2, sub { my $cb = shift; is( $cb->( GET '/' )->code, 200, '[GET /] OK' ); is( $cb->( GET '/' )->content, 'App2', '[GET /] OK content' ); }; } note 'Old format using psgi_app to loop over multiple apps'; { # test global my $app = Dancer2->psgi_app; test_psgi $app, sub { my $cb = shift; is( $cb->( GET '/forward_to_new' )->code, 200, '[GET /forward_to_new] OK', ); is( $cb->( GET '/forward_to_new' )->content, 'New', '[GET /forward_to_new] OK content', ); }; } forward_hmv_params.t100644000765000024 327415154413402 16765 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More import => ['!pass']; use Plack::Test; use HTTP::Request::Common; use Encode qw(decode); use utf8; { package Test::Forward::HMV; use Dancer2; any '/' => sub { 'home:' . join( ',', request->parameters->flatten ); }; get '/get' => sub { forward '/', { get => 'bâz' }; }; post '/post' => sub { forward '/', { post => 'bâz' }; }; post '/change/:me' => sub { forward '/', { post => route_parameters->get('me') }, { method => 'GET' }; }; } my $test = Plack::Test->create( Test::Forward::HMV->to_app ); subtest 'query parameters (#1245)' => sub { my $res = $test->request( GET '/get?foo=bâr' ); is $res->code, 200, "success forward for /get"; my $content = decode( 'UTF-8', $res->content ); is $content, 'home:foo,bâr,get,bâz', "query parameters merged after forward"; }; subtest 'body parameters (#1116)' => sub { my $res = $test->request( POST '/post', { foo => 'bâr' } ); is $res->code, 200, "success forward for /post"; # The order is important: post,baz are QUERY params # foo,bar are the original body params my $content = decode( 'UTF-8', $res->content ); like $content, qr/^home:post,bâz/, "forward params become query params"; is $content, 'home:post,bâz,foo,bâr', "body parameters available after forward"; }; subtest 'params when method changes' => sub { my $res = $test->request( POST '/change/1234', { foo => 'bâr' } ); is $res->code, 200, "success forward for /change/:me"; my $content = decode( 'UTF-8', $res->content ); is $content, 'home:post,1234,foo,bâr', "body parameters available after forward"; }; done_testing(); serializer_mutable.t100644000765000024 701015154413402 16756 0ustar00jasonstaff000000000000Dancer2-2.1.0/tuse strict; use warnings; use Test::More tests => 5 ; use Dancer2::Serializer::Mutable; use Plack::Test; use HTTP::Request::Common; use Encode; use JSON::MaybeXS; use YAML; use Ref::Util qw; { package MyApp; use Dancer2; use Ref::Util qw; set serializer => 'Mutable'; get '/serialize' => sub { +{ bar => 'baz' } }; post '/deserialize' => sub { return request->data && is_hashref( request->data ) && request->data->{bar} ? { bar => request->data->{bar} } : { ret => '?' }; }; } my $test = Plack::Test->create( MyApp->to_app ); subtest "serializer returns to default state" => sub { my $res = $test->request( GET '/serialize' ); is( $res->headers->content_type, 'application/json', "Default content-type header", ); $res = $test->request( GET '/serialize', 'Accept' => 'text/x-data-dumper' ); is( $res->headers->content_type, 'text/x-data-dumper', "Correct content-type header", ); $res = $test->request( GET '/serialize' ); is( $res->headers->content_type, 'application/json', "Correct default content-type header after a request that used another", ); }; # Configure test content-type cases my $d = { yaml => { types => [ qw(text/x-yaml text/html) ], value => encode('UTF-8', YAML::Dump({ bar => 'baz' })), last_val => "---bar:baz", }, dumper => { types => [ qw(text/x-data-dumper) ], value => Data::Dumper::Dumper({ bar => 'baz' }), last_val => "\$VAR1={'bar'=>'baz'};", }, json => { types => [ qw(text/x-json application/json) ], value => JSON::MaybeXS::encode_json({ bar => 'baz' }), last_val => '{"bar":"baz"}', }, default => { types => [ '*/*', '' ], value => JSON::MaybeXS::encode_json({ bar => 'baz' }), last_val => '{"bar":"baz"}', return_content_type => 'application/json', }, }; for my $format (keys %$d) { subtest "Format: $format" => sub { my $s = $d->{$format}; # Response with implicit call to the serializer for my $content_type ( @{ $s->{types} } ) { for my $ct (qw/Content-Type Accept/) { # Test getting the value serialized in the correct format my $res = $test->request( GET '/serialize', $ct => $content_type ); is( $res->code, 200, "[/$format] Correct status" ); is( $res->content, $s->{value}, "[/$format] Correct content" ); is( $res->headers->content_type, $s->{return_content_type} || $content_type, "[/$format] Correct content-type headers", ); } # Test sending the value serialized in the correct format # needs to be de-serialized and returned my $req = $test->request( POST '/deserialize', 'Content-Type' => $content_type, content => $s->{value} ); my $content = $req->content; $content =~ s/\s//g; is( $req->code, 200, "[/$format] Deserialize: correct status" ); is( $content, $s->{last_val}, "[/$format] Deserialize: correct content" ); } #/ for my $content_type }; #/ subtest } #/ for my $format pretty000755000765000024 015154413402 15413 5ustar00jasonstaff000000000000Dancer2-2.1.0/t/corpus505.tt100644000765000024 10015154413402 16404 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/corpus/prettyTemplate selected. message: [% content %] status: [% status %] folder000755000765000024 015154413402 15161 5ustar00jasonstaff000000000000Dancer2-2.1.0/t/viewspage.tt100644000765000024 2315154413402 16541 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/views/folderPage under folder. error_template.t100644000765000024 370515154413402 16711 0ustar00jasonstaff000000000000Dancer2-2.1.0/t/dsluse strict; use warnings; use Test::More; use Plack::Test; use HTTP::Request::Common; use Ref::Util qw; { package CustomError; use Dancer2; set views => 't/corpus/pretty'; set public_dir => 't/corpus/pretty_public'; get '/error' => sub { send_error "oh my", 505; }; get '/public' => sub { send_error "static", 510; }; } { package StandardError; use Dancer2; set show_stacktrace => 1; get '/no_template' => sub { send_error "oopsie", 404; }; } my $custom_error_app = CustomError->to_app; my $standard_error_app = StandardError->to_app; ok( is_coderef($custom_error_app), 'Got app' ); ok( is_coderef($standard_error_app), 'Got app' ); my $custom_error_test = Plack::Test->create($custom_error_app); my $standard_error_test = Plack::Test->create($standard_error_app); subtest "/error" => sub { my $res = $custom_error_test->request( GET '/error' ); is $res->code, 505, 'send_error sets the status to 505'; like $res->content, qr{Template selected}, 'Error message looks good'; like $res->content, qr{message: oh my}; like $res->content, qr{status: 505}; }; subtest "/public" => sub { my $res = $custom_error_test->request( GET '/public' ); is $res->code, 510, 'send_error sets the status to 510'; like $res->content, qr{Static page}, 'Error message looks good'; }; subtest '404 with static template' => sub { my $res = $custom_error_test->request( GET '/middle/of/nowhere' ); is $res->code, 404, 'unknown route => 404'; like $res->content, qr{you're lost}i, 'Error message looks good'; }; subtest "/no_template" => sub { my $res = $standard_error_test->request( GET '/no_template' ); is $res->code, 404, 'send_error sets the status to 404'; like $res->content, qr{

Error 404 - Not Found

}, 'Error message looks good'; unlike $res->content, qr{Stack}, 'Error contains no stack trace'; }; done_testing; Policy.pod100644000765000024 764015154413402 16444 0ustar00jasonstaff000000000000Dancer2-2.1.0/lib/Dancer2package Dancer2::Policy; # ABSTRACT: Dancer core and community policy and standards of conduct use strict; use warnings; 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Policy - Dancer core and community policy and standards of conduct =head1 VERSION version 2.1.0 =head1 DESCRIPTION This document describes various policies (most notably, the standards of conduct) for the Dancer core developers and broad community. This is what we expect from our community and ourselves and these are the standards of behavior we set forth in order to make sure the community remains a safe space for all of its members, without exception. =head1 STANDARDS OF CONDUCT These standards apply anywhere the community comes together as a group. This includes, but is not limited to, the Dancer IRC channel, the Dancer mailing list, Dancer hackathons, and Dancer conferences. =over 4 =item * Always be civil. =item * Heed the moderators. =item * Abuse is not tolerated. =back Civility is simple: stick to the facts while avoiding demeaning remarks and sarcasm. It is not enough to be factual. You must also be civil. Responding in kind to incivility is not acceptable. If the list moderators tell you that you are not being civil, carefully consider how your words have appeared before responding in any way. You may protest, but repeated protest in the face of a repeatedly reaffirmed decision is not acceptable. Unacceptable behavior will result in a public and clearly identified warning. Repeated unacceptable behavior will result in removal from the mailing list and revocation of any commit bit. The first removal is for one month. Subsequent removals will double in length. After six months with no warning, a user's ban length is reset. Removals, like warnings, are public. The list of moderators consists of all active core developers. This includes, in alphabetical order, Alberto Simões, David Precious, Jason Crome, Mickey Nasriachi, Peter Mottram, Russell Jenkins, Sawyer X, Stefan Hornburg (Racke), and Yanick Champoux. This list might additionally grow to active members of the community who have stepped up to help handle abusive behavior. If this should happen, this document would be updated to include their names. Additionally, it's important to understand the self-regulating nature we foster at the Dancer community. This means anyone and everyone in the community - in the channel, on the list, at an event - has the ability to call out unacceptable behavior and incivility to others in the community. Moderators are responsible for issuing warnings and take disciplinary actions, but anyone may - and is encouraged - to publicly make note of unacceptable treatment of others. As a core principle, abuse is never tolerated. One cannot berate, insult, debase, deride, put down, or vilify anyone, or act towards anyone in a way intending to hurt them. The community specifically considers as abuse any attempts to otherize anyone by any individual characteristic, including, but not limited to, their technical skill, knowledge or by their age, colour, disability, gender, language, national or social origin, political or other opinion, race, religion, sex, or sexual orientation. The community aims to maintain a safe space for everyone, in any forum it has. If you ever feel this core principle has been compromised, you are strongly urged to contact a moderator. We are always here. Remember, this is B community, as much as it is anyone else's. =head1 CREDITS This policy has been adopted and adapted from the policy available for the Perl language development, provided by B (the Perl 5 Porters). The original inspiration policy document can be read at L. =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2026 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 Manual.pod100644000765000024 22647715154413402 16475 0ustar00jasonstaff000000000000Dancer2-2.1.0/lib/Dancer2# ABSTRACT: A guide to building web applications with Dancer2 package Dancer2::Manual; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Manual - A guide to building web applications with Dancer2 =head1 VERSION version 2.1.0 =head1 Introduction to Dancer2: Managing Danceyland Welcome to Danceyland! As the new manager of this amazing park, you'll be maintaining and enhancing the experience for all your visitors. Imagine each attraction, food stall, and ticket booth as different locations in your web application. Let's explore how to manage these various components using Dancer2. If you're not sure if you're at the correct spot in the park, the L can help you find your way. =head2 What is Dancer2? Dancer2 is a free and open-source web application framework written in Perl. It’s a complete rewrite of the original L, designed to be powerful and flexible, yet incredibly easy to use. With Dancer2, getting your web app up and running is a breeze. It boasts a rich ecosystem of adapters for popular template engines, session storage solutions, logging methods, serializers, and plugins. This means you can build your app your way, effortlessly. In this guide, we'll leverage those strengths to build and manage Danceyland. Before we learn the ins and outs of managing your park, make sure you head over to the L and get your development machine set up with Dancer2. Once it's installed, make sure to build your park: dancer2 gen -a Danceyland =head1 Routes: Different Attractions and Facilities Core to Dancer2 is the concept of routes. Each attraction in your theme park is like a route in your web application. Visitors (users) can navigate to different attractions, just as they would visit different routes in your app. Let's show some of the rides and facilities in our park: # Defining routes for our theme park get '/' => sub { return "Welcome to Danceyland!"; }; get '/roller-coaster' => sub { return "Enjoy the thrilling roller coaster ride!"; }; post '/buy-ticket' => sub { my $ticket = body_parameters->get('ticket'); # Do something with ticket data return "You bought a $ticket ticket!"; }; =over =item The `/` route is like the main entrance to our theme park. Visitors are greeted with a welcome message. =item The `/roller-coaster` route is a thrilling ride. When visitors take this path, they get a special message. =item The `/buy-ticket` route is the ticket booth. Visitors buy their tickets here. =back =head2 New Keywords =over =item get This keyword defines a route that responds to HTTP GET requests. It takes two parameters: the route path and a subroutine that defines the response. get '/path' => sub { return "Response text"; }; Note that a route to match C requests is automatically created when you create a C route. =item post This keyword defines a route that responds to HTTP POST requests. It works similarly to C, but is used for actions like submitting forms or buying tickets. post '/path' => sub { my $param = body_parameters->get('param'); return "You submitted: $param"; }; =back So exactly what are these HTTP requests, and what are they all about? =head2 HTTP Methods: Visitor Actions Think of HTTP methods as the different actions visitors can take in your theme park. Entering the park, buying tickets, updating ticket details, and leaving the park are all actions represented by C, C, C, C, C, and C methods respectively. Handling visitor actions in the park: # Defining HTTP methods for visitor actions get '/' => sub { return "Welcome to Danceyland!"; }; post '/buy-ticket' => sub { my $ticket = body_parameters->get('ticket'); return "You bought a $ticket ticket!"; }; put '/update-ticket' => sub { my $new_ticket = body_parameters->get('new_ticket'); return "Your ticket has been updated to $new_ticket!"; }; del '/leave-park' => sub { return "Thank you for visiting! Please come again!"; }; options '/park/info' => sub { return "Allowed methods: GET, POST, PUT"; }; patch '/profile/:id' => sub { my $user_id = route_parameters->get('id'); my $new_email = body_parameters->get('email'); return "Updated profile for user $user_id with email $new_email"; }; =over =item GET: Visitors enter the park and see a welcome message. =item POST: Visitors buy a ticket. =item PUT: Visitors update their ticket details. =item DELETE: Visitors leave the park. =item PATCH: Update part of a visitor's profile (in this case, email). =item OPTIONS: Describing the available operations on a specific route. Keep in mind, you would rarely implement this in your web application. =back These are good conventions to follow, but you can make your route handlers do whatever makes the most sense for your application. =head2 Routes, Route Definitions, and Route Handlers You may hear other Dancer developers talk about "routes", "route definitions", and "route handlers". "Route definitions" refers to the HTTP method and URL to respond to, while "route handler" is only the code implementing functionality for that definition. The two of these together make what's known as a route. get '/' => sub {...}; =over =item The verb, path, and subroutine is a route definition (AKA "route"). =item Only the subroutine reference (C) is the route handler. =item The route definition I the route handler collectively are the route. =back Each route requires a defintion and handler. The route needs to either return a string or Perl data structure to be rendered for the client. We'll learn more about rendering data structures later in our guide to Danceyland; for now, we're going to focus on returning strings, which will be rendered as HTML. =head3 What if we want a single park location to respond to multiple request types? Route definitions 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 '/visitor-center' => sub { # Write code to do something at the visitor center! } The following will match GET or POST requests to C: any ['get', 'post'] => '/visitor-center' => sub { # Write code to do something at the visitor center! }; =head2 URI Generation Dancer2 can generate URIs using the C and C keywords. Letting Dancer2 generate URIs helps ensure consistency, and reduces the amount of maintenance needed by making sure URIs are always up to date as the application evolves over time. =head3 uri_for The C keyword is used to generate a URI for a given path within your application, including query parameters. It's especially useful when you want to construct URLs dynamically inside your routes. =head4 Example: Generating a URI in Danceyland get '/ride/:name' => sub { my $ride_name = route_parameters->get('name'); return 'Enjoy the ride at ' . uri_for("/ride/$ride_name"); }; In this example, C generates a full URI for the ride name in Danceyland. =head3 uri_for_route The C keyword creates a URL for a named route. In Danceyland, it can be used to generate a URL to any part of the park that has a named route. For more information, see L. =head3 Example 1: Basic Usage get 'films' => '/cinemaland/film-gallery' => sub { return "See the films at " . uri_for_route('films'); }; In this example, the route is named C, and C generates a URL pointing to it. =head3 Example 2: Using Route Parameters get 'ride' => '/ride/:name' => sub { my $ride_name = route_parameters->get('name'); return "Ride details: " . uri_for_route('ride', { name => $ride_name }); }; This example uses C to generate a URL that includes the named route parameter C. =head3 Example 3: Including Query Parameters get 'search' => '/search' => sub { return uri_for_route('search', { q => 'roller coaster' }); }; get '/search' => sub { my $query = query_parameters->get('q'); return "Search results for: $query"; }; In this example, C generates a URL for the named route C with the query parameter C set to "roller coaster". =head2 Parameter Handling In Danceyland, visitors will often need to communicate with park staff. Similarly, your apps will need to take in information from application users via parameters. Dancer2 provides several methods for handling different types of parameters. =head3 Keywords for working with parameters =head4 param and params (Not Recommended) The C and C keywords are legacy methods to access request parameters. While still functional, they are not recommended for new applications. Instead, you should use the more specific keywords like C, C, and C for clarity and precision. =head4 Example: Using param (Not Recommended) get '/submit' => sub { # Please use query_parameters() shown below my $name = param('name'); return "Submitted name: $name"; }; =head4 Example: Using params (Not Recommended) get '/all-params' => sub { # Pleaase use query_parameters() shown below my %all_params = params; return "All parameters: " . join(', ', %all_params); }; C and C are included here for completeness but are not the preferred methods for handling parameters in Danceyland. =head4 body_parameters This keyword retrieves parameters from the body of the request, typically used in POST requests. Example of C with multiple values: post '/submit' => sub { my $name = body_parameters->get('name'); my $email = body_parameters->get('email'); return "Submitted name: $name, email: $email"; }; =head4 query_parameters This keyword retrieves parameters from the query string of the request URL: # Matches URL: /search?q=rides&cat=thrill get '/search' => sub { my $query = query_parameters->get('q'); my $category = query_parameters->get('cat'); return "Search query: $query in category: $category"; }; The above route would match the URL C. =head4 route_parameters This keyword retrieves named parameters from the route declaration: # Matches URL: /user/123 get '/user/:id' => sub { my $user_id = route_parameters->get('id'); return "User ID: $user_id"; }; =head4 get_all This method works for all of the above parameter-fetching keywords. If you have a parameter that may contain more than one value, C will return an array reference containing all selected values, even if there is only a single value returned. Example of C: # Matches URL: /all-params?name=John&age=30 get '/all-params' => sub { my $params = query_parameters->get_all(); return "All query parameters: " . join(', ', %$params); }; =head3 Named Route Parameters Named route parameters allow you to capture specific segments of the URL and use them in your route handler: # Matches URL: /ride/roller-coaster get '/ride/:name' => sub { my $ride_name = route_parameters->get('name'); return "Welcome to the $ride_name ride!"; }; Named route parameters are retrieved with the L keyword. =head3 Wildcard Route Parameters Wildcard route parameters allow you to capture arbitrary parts of the URL. There are two types: C and C. C is represented by a single asterisk (C<*>), and megaspat is represented by a double asterisk (C<**>). Examples of wildcard route parameters include: =over =item splat: Captures one segment of the URL # Matches URL: /files/document.txt get '/files/*' => sub { my ($file) = splat; return "You requested the file: $file"; }; =item megasplat: Captures multiple segments of the URL # Matches URL: /files/documents/reports/2023/summary.txt get '/files/**' => sub { my @files = splat; return "You requested the files: " . join(', ', @files); }; =back =head3 Combining named and wildcard parameters You can combine named and wildcard parameters in your routes to capture both specific and arbitrary segments of the URL. Example combining named and wildcard parameters: # Matches URL: /user/123/files/documents/reports/2023/summary.txt get '/user/:id/files/**' => sub { my $user_id = route_parameters->get('id'); my @files = splat; return "User ID: $user_id requested the files: " . join(', ', @files); }; =head3 Named parameters with type constraints Type constraints allow you to enforce specific types for named parameters, ensuring that the parameters meet certain criteria. Dancer2 natively supports named parameters with type constraints without needing to rely on external plugins. Here’s how to declare and use type constraints for named route parameters in Dancer2: use Dancer2; get '/ride/:id[Int]' => sub { my $id = route_parameters->get('id'); return "Ride ID: $id"; }; get '/guest/:name[Str]' => sub { my $name = route_parameters->get('name'); return "Guest Name: $name"; }; MyApp->to_app(); =over 4 =item * B: This constraint ensures that the C parameter must be an integer. =item * B: This ensures that the C parameter must be a string. =back Note: For more complex parameter types, the C module provides additional constraints and validation for all parameter types. =head3 Wildcard Parameters with Type Constraints You can also enforce type constraints on wildcard parameters: # Matches URL: /images/photo.jpg get '/images/*.*[ArrayRef[Str]]' => sub { my ($filename, $extension) = splat; return "Filename: $filename, Extension: $extension"; }; # Matches URL: /documents/folder/subfolder/file.txt get '/documents/**[ArrayRef[Str]]' => sub { my @path = splat; return "Document path: " . join('/', @path); }; =head3 Regex Route Matching Regex route matching allows you to define routes using regular expressions, providing more flexibility in matching URLs: # Matches URL: /product/12345 get qr{/product/(\d+)} => sub { my ($product_id) = splat; return "Product ID: $product_id"; }; # Matches URL: /category/electronics get qr{/category/(\w+)} => sub { my ($category_name) = splat; return "Category: $category_name"; }; It is also possible to use named captures in regular expressions: # Matches URL: /product/12345 get qr{/product/(?\d+)} => sub { my $product_id = captures->{'product_id'}; return "Product ID: $product_id"; }; =head3 Combining Examples # Matches URL: /item/42/specifications get '/item/:id[Int]/*[ArrayRef[Str]]' => sub { my $item_id = route_parameters->get('id'); my ($detail) = splat; return "Item ID: $item_id, Detail: $detail"; }; # Matches URL: /archive/2023/07/10 get qr{/archive/(\d{4})/(\d{2})/(\d{2})} => sub { my ($year, $month, $day) = splat; return "Archive for: $year-$month-$day"; }; =head2 Organizing routes and growing your app using prefix The prefix DSL keyword helps you group related routes. For example, you can organize all the routes of a given section in Danceyland with the C keyword as such: package MyApp; use Dancer2; # Prefix for Cinemaland prefix '/cinemaland' => sub { get '/film-gallery' => sub { return "Welcome to Cinemaland! Here you can learn about famous movies."; }; get '/movie-schedule' => sub { return "Movie Schedule: 10 AM and 4 PM."; }; }; # Prefix for Actionland prefix '/actionland' => sub { get '/thrill-rides' => sub { return "Welcome to Actionland! Enjoy the thrill rides."; }; get '/roller-coaster' => sub { return "The best roller coaster in the park!"; }; }; MyApp->to_app(); This example organizes routes by grouping all cinema-related activities under C and all rides under C. =head2 Controlling the flow with forward, redirect, and pass These DSL keywords in Dancer2 allow you to manage the flow of actions within your routes. Here’s how they work in Danceyland. =head3 forward C allows you to forward the current request to another route handler, as if it were redirected, but without sending a new HTTP request. Put another way, a different route than the one requested is processed, but the address in the user's browser's bar stays the same. =head4 Example get '/guest-services' => sub { forward '/info'; # Forwards to /info }; get '/info' => sub { return "Welcome to Guest Services. How can we help you today?"; }; In this example, when a visitor goes to C, they are forwarded to the C route internally, however, the vistor's browser still shows they are in C. =head3 redirect C sends an HTTP redirect to the client, instructing their browser to make a new request to a different URL. At the end of the request, the user's browser will show a different URL than the one they originally navigated to. =head4 Example get '/old-roller-coaster' => sub { redirect '/new-roller-coaster'; }; get '/new-roller-coaster' => sub { return "Welcome to the new and improved roller coaster!"; }; When a visitor requests C, they are redirected to C. The browser's URL will now show C. =head3 pass C tells Dancer2 to skip the current route and continue looking for the next matching route. =head4 Example get '/vip-area' => sub { if (!session('is_vip')) { pass; # Skip to the next matching route if the user is not VIP } return "Welcome to the VIP area!"; }; get '/vip-area' => sub { return "Access Denied. This area is for VIPs only."; }; In this example, if the session doesn’t indicate the user is a VIP, the request will skip to the next matching route, which denies access. =head1 Templates: Displaying Information, and More! Templates in a web application help present information in a structured and visually appealing way, much like maps, schedules, and banners in a theme park. =head2 Why do we use templates? Templates help separate your display logic from your programming logic. The same code that sends JSON to satisfy an API quest could also be used to display an HTML page containing park information to a user. If the code needed to display HTML is intertwined with the code to gather the data needed, the code necessary to just produce JSON becomes unnecessarily complicated. Templates take the HTML out of your Perl code. =head2 Views In Dancer2, "views" refer to the template files that define the structure of the content presented to the users. Views are typically HTML files with embedded placeholders that are dynamically filled with data when rendered. =head3 How Views Work Suppose you have a template file called C:

<% ride_name %> Ride

Enjoy the thrilling <% ride_name %> ride at Danceyland!

You can use this view in your route handler: get '/ride/:name' => sub { my $ride_name = route_parameters->get('name'); template 'ride' => { ride_name => $ride_name }; }; =head3 Example: Displaying a welcome message on a pretty banner get '/' => sub { template 'welcome', { message => "Welcome to Danceyland!" }; }; In this example, the C route uses a template to display a welcome message. =head3 Example: Displaying the park map get '/map' => sub { template 'map', { attractions => \@attractions }; }; =head3 Example: Using templates to display various information get '/info' => sub { template 'info', { title => "Park Information", content => "Here you can find all the information about Danceyland.", hours => "Open from 10 AM to 8 PM", contact => "Call us at 123-456-7890", }; }; =over 4 =item * The C route uses a template to display a welcome message. =item * The C route uses a template to display the park's map. =item * The C route uses a template to display various pieces of information about the park. =back =head2 New Keywords =head3 template The C