summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Morrott <nickm@debian.org>2020-01-02 21:37:21 +0100
committerNick Morrott <nickm@debian.org>2020-01-02 21:37:21 +0100
commitcb1bc58155756753c8551d4e877971cbe529df58 (patch)
tree8e0da03f1e1c2c0edbc554bb720951ba0c74497d
Import libdancer2-perl_0.300000+dfsg.orig.tar.xz
[dgit import orig libdancer2-perl_0.300000+dfsg.orig.tar.xz]
-rw-r--r--AUTHORS2
-rw-r--r--Changes1671
-rw-r--r--GitGuide.md270
-rw-r--r--LICENSE379
-rw-r--r--MANIFEST387
-rw-r--r--META.json389
-rw-r--r--META.yml274
-rw-r--r--Makefile.PL217
-rw-r--r--cpanfile95
-rw-r--r--examples/single/hello_world.psgi7
-rw-r--r--examples/single/simple_calculator.psgi36
-rw-r--r--lib/Dancer2.pm395
-rw-r--r--lib/Dancer2/CLI.pm35
-rw-r--r--lib/Dancer2/CLI/Command/gen.pm323
-rw-r--r--lib/Dancer2/CLI/Command/version.pm48
-rw-r--r--lib/Dancer2/Config.pod684
-rw-r--r--lib/Dancer2/Cookbook.pod995
-rw-r--r--lib/Dancer2/Core.pm51
-rw-r--r--lib/Dancer2/Core/App.pm1902
-rw-r--r--lib/Dancer2/Core/Cookie.pm250
-rw-r--r--lib/Dancer2/Core/DSL.pm558
-rw-r--r--lib/Dancer2/Core/Dispatcher.pm141
-rw-r--r--lib/Dancer2/Core/Error.pm596
-rw-r--r--lib/Dancer2/Core/Factory.pm49
-rw-r--r--lib/Dancer2/Core/HTTP.pm204
-rw-r--r--lib/Dancer2/Core/Hook.pm112
-rw-r--r--lib/Dancer2/Core/MIME.pm177
-rw-r--r--lib/Dancer2/Core/Request.pm1148
-rw-r--r--lib/Dancer2/Core/Request/Upload.pm178
-rw-r--r--lib/Dancer2/Core/Response.pm406
-rw-r--r--lib/Dancer2/Core/Response/Delayed.pm175
-rw-r--r--lib/Dancer2/Core/Role/ConfigReader.pm357
-rw-r--r--lib/Dancer2/Core/Role/DSL.pm130
-rw-r--r--lib/Dancer2/Core/Role/Engine.pm71
-rw-r--r--lib/Dancer2/Core/Role/Handler.pm52
-rw-r--r--lib/Dancer2/Core/Role/HasLocation.pm104
-rw-r--r--lib/Dancer2/Core/Role/Hookable.pm146
-rw-r--r--lib/Dancer2/Core/Role/Logger.pm345
-rw-r--r--lib/Dancer2/Core/Role/Serializer.pm165
-rw-r--r--lib/Dancer2/Core/Role/SessionFactory.pm503
-rw-r--r--lib/Dancer2/Core/Role/SessionFactory/File.pm189
-rw-r--r--lib/Dancer2/Core/Role/StandardResponses.pm70
-rw-r--r--lib/Dancer2/Core/Role/Template.pm367
-rw-r--r--lib/Dancer2/Core/Route.pm373
-rw-r--r--lib/Dancer2/Core/Runner.pm277
-rw-r--r--lib/Dancer2/Core/Session.pm153
-rw-r--r--lib/Dancer2/Core/Time.pm201
-rw-r--r--lib/Dancer2/Core/Types.pm149
-rw-r--r--lib/Dancer2/FileUtils.pm243
-rw-r--r--lib/Dancer2/Handler/AutoPage.pm127
-rw-r--r--lib/Dancer2/Handler/File.pm179
-rw-r--r--lib/Dancer2/Logger/Capture.pm141
-rw-r--r--lib/Dancer2/Logger/Capture/Trap.pm94
-rw-r--r--lib/Dancer2/Logger/Console.pm62
-rw-r--r--lib/Dancer2/Logger/Diag.pm55
-rw-r--r--lib/Dancer2/Logger/File.pm173
-rw-r--r--lib/Dancer2/Logger/Note.pm58
-rw-r--r--lib/Dancer2/Logger/Null.pm47
-rw-r--r--lib/Dancer2/Manual.pod3368
-rw-r--r--lib/Dancer2/Manual/Deployment.pod732
-rw-r--r--lib/Dancer2/Manual/Migration.pod606
-rw-r--r--lib/Dancer2/Manual/Testing.pod144
-rw-r--r--lib/Dancer2/Plugin.pm1107
-rw-r--r--lib/Dancer2/Plugins.pod136
-rw-r--r--lib/Dancer2/Policy.pod121
-rw-r--r--lib/Dancer2/Serializer/Dumper.pm101
-rw-r--r--lib/Dancer2/Serializer/JSON.pm153
-rw-r--r--lib/Dancer2/Serializer/Mutable.pm246
-rw-r--r--lib/Dancer2/Serializer/YAML.pm91
-rw-r--r--lib/Dancer2/Session/Simple.pm93
-rw-r--r--lib/Dancer2/Session/YAML.pm95
-rw-r--r--lib/Dancer2/Template/Implementation/ForkedTiny.pm390
-rw-r--r--lib/Dancer2/Template/Simple.pm220
-rw-r--r--lib/Dancer2/Template/TemplateToolkit.pm245
-rw-r--r--lib/Dancer2/Template/Tiny.pm119
-rw-r--r--lib/Dancer2/Test.pm978
-rw-r--r--lib/Dancer2/Tutorial.pod722
-rwxr-xr-xscript/dancer2208
-rw-r--r--share/skel/.dancer0
-rw-r--r--share/skel/MANIFEST.SKIP16
-rw-r--r--share/skel/Makefile.PL26
-rw-r--r--share/skel/bin/+app.psgi45
-rw-r--r--share/skel/config.yml62
-rw-r--r--share/skel/cpanfile11
-rw-r--r--share/skel/environments/development.yml23
-rw-r--r--share/skel/environments/production.yml16
-rw-r--r--share/skel/lib/AppFile.pm10
-rwxr-xr-xshare/skel/public/+dispatch.cgi16
-rw-r--r--share/skel/public/+dispatch.fcgi18
-rw-r--r--share/skel/public/404.html18
-rw-r--r--share/skel/public/500.html18
-rw-r--r--share/skel/public/css/error.css85
-rw-r--r--share/skel/public/css/style.css189
-rw-r--r--share/skel/public/favicon.icobin0 -> 1406 bytes
-rw-r--r--share/skel/public/images/perldancer-bg.jpgbin0 -> 7125 bytes
-rw-r--r--share/skel/public/images/perldancer.jpgbin0 -> 2240 bytes
-rw-r--r--share/skel/t/001_base.t5
-rw-r--r--share/skel/t/002_index_route.t16
-rw-r--r--share/skel/views/index.tt148
-rw-r--r--share/skel/views/layouts/main.tt22
-rw-r--r--t/00-compile.t153
-rw-r--r--t/00-report-prereqs.dd127
-rw-r--r--t/00-report-prereqs.t193
-rw-r--r--t/app.t216
-rw-r--r--t/app/t1/bin/app.psgi6
-rw-r--r--t/app/t1/config.yml2
-rw-r--r--t/app/t1/lib/App1.pm6
-rw-r--r--t/app/t1/lib/Sub/App2.pm6
-rw-r--r--t/app/t2/.dancer1
-rw-r--r--t/app/t2/config.yml2
-rw-r--r--t/app/t2/lib/App3.pm5
-rw-r--r--t/app_alone.t25
-rw-r--r--t/author-no-tabs.t366
-rw-r--r--t/author-pod-syntax.t15
-rw-r--r--t/auto_page.t99
-rw-r--r--t/caller.t30
-rw-r--r--t/charset_server.t58
-rw-r--r--t/classes/Dancer2-Core-Factory/new.t16
-rw-r--r--t/classes/Dancer2-Core-Hook/new.t69
-rw-r--r--t/classes/Dancer2-Core-Request/new.t521
-rw-r--r--t/classes/Dancer2-Core-Request/serializers.t74
-rw-r--r--t/classes/Dancer2-Core-Response-Delayed/after_hooks.t64
-rw-r--r--t/classes/Dancer2-Core-Response-Delayed/new.t70
-rw-r--r--t/classes/Dancer2-Core-Response/new_from.t71
-rw-r--r--t/classes/Dancer2-Core-Role-Engine/with.t21
-rw-r--r--t/classes/Dancer2-Core-Role-Handler/with.t19
-rw-r--r--t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists0
-rw-r--r--t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists0
-rw-r--r--t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer0
-rw-r--r--t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl0
-rw-r--r--t/classes/Dancer2-Core-Role-HasLocation/with.t105
-rw-r--r--t/classes/Dancer2-Core-Role-Serializer/with.t164
-rw-r--r--t/classes/Dancer2-Core-Role-StandardResponses/with.t120
-rw-r--r--t/classes/Dancer2-Core-Route/base.t67
-rw-r--r--t/classes/Dancer2-Core-Route/deprecated_param_keys.t34
-rw-r--r--t/classes/Dancer2-Core-Route/match.t437
-rw-r--r--t/classes/Dancer2-Core-Runner/environment.t42
-rw-r--r--t/classes/Dancer2-Core-Runner/new.t232
-rw-r--r--t/classes/Dancer2-Core-Runner/psgi_app.t89
-rw-r--r--t/classes/Dancer2-Core/camelize.t22
-rw-r--r--t/classes/Dancer2/import-pragmas.t18
-rw-r--r--t/classes/Dancer2/import.t270
-rw-r--r--t/config.yml6
-rw-r--r--t/config/config.yml6
-rw-r--r--t/config/environments/failure.yml3
-rw-r--r--t/config/environments/merging.yml3
-rw-r--r--t/config/environments/production.yml2
-rw-r--r--t/config/environments/staging.json5
-rw-r--r--t/config2/config.yml9
-rw-r--r--t/config2/config_local.yml4
-rw-r--r--t/config2/environments/lconfig.yml3
-rw-r--r--t/config2/environments/lconfig_local.yml3
-rw-r--r--t/config_multiapp.t24
-rw-r--r--t/config_reader.t189
-rw-r--r--t/config_settings.t31
-rw-r--r--t/context-in-before.t57
-rw-r--r--t/cookie.t207
-rw-r--r--t/corpus/pretty/505.tt4
-rw-r--r--t/corpus/pretty/relative.tt3
-rw-r--r--t/corpus/pretty_public/404.html1
-rw-r--r--t/corpus/pretty_public/510.html1
-rw-r--r--t/corpus/static/1x1.pngbin0 -> 95 bytes
-rw-r--r--t/corpus/static/index.html10
-rw-r--r--t/custom_dsl.t30
-rw-r--r--t/dancer-test.t153
-rw-r--r--t/dancer-test/config.yml1
-rw-r--r--t/deserialize.t215
-rw-r--r--t/disp_named_capture.t35
-rw-r--r--t/dispatcher.t251
-rw-r--r--t/dsl/any.t48
-rw-r--r--t/dsl/app.t18
-rw-r--r--t/dsl/content.t49
-rw-r--r--t/dsl/delayed.t161
-rw-r--r--t/dsl/error_template.t78
-rw-r--r--t/dsl/extend.t53
-rw-r--r--t/dsl/extend_config/config.yml1
-rw-r--r--t/dsl/halt.t86
-rw-r--r--t/dsl/halt_with_param.t85
-rw-r--r--t/dsl/json.t20
-rw-r--r--t/dsl/parameters.t396
-rw-r--r--t/dsl/pass.t45
-rw-r--r--t/dsl/path.t125
-rw-r--r--t/dsl/request.t42
-rw-r--r--t/dsl/route_retvals.t15
-rw-r--r--t/dsl/send_as.t161
-rw-r--r--t/dsl/send_file.t149
-rw-r--r--t/dsl/splat.t30
-rw-r--r--t/dsl/to_app.t31
-rw-r--r--t/engine.t62
-rw-r--r--t/error.t264
-rw-r--r--t/examples/hello_world.t18
-rw-r--r--t/examples/simple_calculator.t41
-rw-r--r--t/factory.t21
-rw-r--r--t/file_utils.t93
-rw-r--r--t/forward.t183
-rw-r--r--t/forward_before_hook.t58
-rw-r--r--t/forward_hmv_params.t57
-rw-r--r--t/forward_test_tcp.t82
-rw-r--r--t/hooks.t237
-rw-r--r--t/http_methods.t49
-rw-r--r--t/http_status.t41
-rw-r--r--t/issues/config.yml1
-rw-r--r--t/issues/gh-1013/gh-1013.t39
-rw-r--r--t/issues/gh-1013/views/t.tt1
-rw-r--r--t/issues/gh-1046/config.yml1
-rw-r--r--t/issues/gh-1046/gh-1046.t23
-rw-r--r--t/issues/gh-1070.t22
-rw-r--r--t/issues/gh-1098.t93
-rw-r--r--t/issues/gh-1216/gh-1216.t19
-rw-r--r--t/issues/gh-1216/lib/App.pm4
-rw-r--r--t/issues/gh-1216/lib/App/Extra.pm7
-rw-r--r--t/issues/gh-1216/lib/Dancer2/Plugin/Null.pm4
-rw-r--r--t/issues/gh-1226/gh-1226.t26
-rw-r--r--t/issues/gh-1226/lib/App.pm4
-rw-r--r--t/issues/gh-1226/lib/App/Extra.pm11
-rw-r--r--t/issues/gh-1226/lib/Dancer2/Plugin/Test/AccessDSL.pm54
-rw-r--r--t/issues/gh-1230/gh-1230.t26
-rw-r--r--t/issues/gh-1230/lib/App.pm4
-rw-r--r--t/issues/gh-1230/lib/App/Extra.pm11
-rw-r--r--t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessDSL.pm60
-rw-r--r--t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessPluginDSL.pm33
-rw-r--r--t/issues/gh-1232.t58
-rw-r--r--t/issues/gh-596.t20
-rw-r--r--t/issues/gh-634.t119
-rw-r--r--t/issues/gh-639/fails/.dancer0
-rw-r--r--t/issues/gh-639/fails/config.yml3
-rw-r--r--t/issues/gh-639/fails/issue.t13
-rw-r--r--t/issues/gh-639/succeeds/.dancer0
-rw-r--r--t/issues/gh-639/succeeds/config.yml4
-rw-r--r--t/issues/gh-639/succeeds/issue.t13
-rw-r--r--t/issues/gh-650/gh-650.t49
-rw-r--r--t/issues/gh-650/views/environment_setting.tt1
-rw-r--r--t/issues/gh-723.t55
-rw-r--r--t/issues/gh-730.t69
-rw-r--r--t/issues/gh-762.t41
-rw-r--r--t/issues/gh-762/views/404.tt4
-rw-r--r--t/issues/gh-794.t27
-rw-r--r--t/issues/gh-797.t56
-rw-r--r--t/issues/gh-799.t64
-rw-r--r--t/issues/gh-811.t56
-rw-r--r--t/issues/gh-931.t75
-rw-r--r--t/issues/gh-936.t33
-rw-r--r--t/issues/gh-936/views/error.tt1
-rw-r--r--t/issues/gh-944.t43
-rw-r--r--t/issues/gh-975/config.yml1
-rw-r--r--t/issues/gh-975/gh-975.t15
-rw-r--r--t/issues/gh-975/test_public_dir/test.txt1
-rw-r--r--t/issues/memleak/die_in_hooks.t55
-rw-r--r--t/issues/vars-in-forward.t24
-rw-r--r--t/lib/App1.pm15
-rw-r--r--t/lib/App2.pm15
-rw-r--r--t/lib/Dancer2/Plugin/Bar.pm23
-rw-r--r--t/lib/Dancer2/Plugin/DancerPlugin.pm28
-rw-r--r--t/lib/Dancer2/Plugin/DefineKeywords.pm32
-rw-r--r--t/lib/Dancer2/Plugin/EmptyPlugin.pm10
-rw-r--r--t/lib/Dancer2/Plugin/Foo.pm52
-rw-r--r--t/lib/Dancer2/Plugin/FooPlugin.pm62
-rw-r--r--t/lib/Dancer2/Plugin/Hookee.pm18
-rw-r--r--t/lib/Dancer2/Plugin/OnPluginImport.pm26
-rw-r--r--t/lib/Dancer2/Plugin/PluginWithImport.pm27
-rw-r--r--t/lib/Dancer2/Plugin/Polite.pm44
-rw-r--r--t/lib/Dancer2/Session/SimpleNoChangeId.pm41
-rw-r--r--t/lib/Foo.pm8
-rw-r--r--t/lib/MyDancerDSL.pm36
-rw-r--r--t/lib/PoC/Plugin/Polite.pm27
-rw-r--r--t/lib/SubApp1.pm14
-rw-r--r--t/lib/SubApp2.pm14
-rw-r--r--t/lib/TestApp.pm125
-rw-r--r--t/lib/TestPod.pm85
-rw-r--r--t/lib/TestTypeLibrary.pm8
-rw-r--r--t/lib/poc.pm22
-rw-r--r--t/lib/poc2.pm32
-rw-r--r--t/log_die_before_hook.t38
-rw-r--r--t/log_levels.t128
-rw-r--r--t/logger.t116
-rw-r--r--t/logger_console.t34
-rw-r--r--t/memory_cycles.t29
-rw-r--r--t/mime.t42
-rw-r--r--t/multi_apps.t48
-rw-r--r--t/multi_apps_forward.t83
-rw-r--r--t/multiapp_template_hooks.t208
-rw-r--r--t/named_apps.t40
-rw-r--r--t/no_default_middleware.t59
-rw-r--r--t/plugin2/basic-2.t37
-rw-r--r--t/plugin2/basic.t31
-rw-r--r--t/plugin2/define-keywords.t28
-rw-r--r--t/plugin2/find_plugin.t104
-rw-r--r--t/plugin2/from-config.t92
-rw-r--r--t/plugin2/hooks.t82
-rw-r--r--t/plugin2/inside-plugin.t73
-rw-r--r--t/plugin2/keywords-hooks-namespace.t63
-rw-r--r--t/plugin2/memory_cycles.t36
-rw-r--r--t/plugin2/no-app-munging.t23
-rw-r--r--t/plugin2/no-clobbering.t63
-rw-r--r--t/plugin2/no-config.t38
-rw-r--r--t/plugin2/with-plugins.t71
-rw-r--r--t/plugin_import.t60
-rw-r--r--t/plugin_multiple_apps.t34
-rw-r--r--t/plugin_register.t56
-rw-r--r--t/plugin_syntax.t184
-rw-r--r--t/prepare_app.t61
-rw-r--r--t/psgi_app.t92
-rw-r--r--t/psgi_app_forward_and_pass.t49
-rw-r--r--t/public/file.txt1
-rw-r--r--t/redirect.t244
-rw-r--r--t/release-distmeta.t14
-rw-r--r--t/request.t273
-rw-r--r--t/request_make_forward_to.t38
-rw-r--r--t/request_upload.t196
-rw-r--r--t/response.t65
-rw-r--r--t/roles/hook.t67
-rw-r--r--t/route-pod-coverage/route-pod-coverage.t27
-rw-r--r--t/scope_problems/config.yml1
-rw-r--r--t/scope_problems/dispatcher_internal_request.t32
-rw-r--r--t/scope_problems/keywords_before_template_hook.t43
-rw-r--r--t/scope_problems/session_is_cleared.t65
-rw-r--r--t/scope_problems/views/500.tt1
-rw-r--r--t/scope_problems/with_return_dies.t72
-rw-r--r--t/serializer.t42
-rw-r--r--t/serializer_json.t75
-rw-r--r--t/serializer_mutable.t117
-rw-r--r--t/serializer_mutable_custom.t126
-rw-r--r--t/session_bad_client_cookie.t119
-rw-r--r--t/session_config.t86
-rw-r--r--t/session_engines.t117
-rw-r--r--t/session_forward.t195
-rw-r--r--t/session_hooks.t190
-rw-r--r--t/session_hooks_no_change_id.t195
-rw-r--r--t/session_in_template.t98
-rw-r--r--t/session_lifecycle.t227
-rw-r--r--t/session_object.t50
-rw-r--r--t/shared_engines.t51
-rw-r--r--t/static_content.t30
-rw-r--r--t/template.t199
-rw-r--r--t/template_default_tokens.t55
-rw-r--r--t/template_ext.t29
-rw-r--r--t/template_name.t29
-rw-r--r--t/template_simple.t89
-rw-r--r--t/template_tiny/01_compile.t11
-rw-r--r--t/template_tiny/02_trivial.t34
-rw-r--r--t/template_tiny/03_samples.t110
-rw-r--r--t/template_tiny/04_compat.t107
-rw-r--r--t/template_tiny/05_preparse.t69
-rw-r--r--t/template_tiny/samples/01_hello.tt8
-rw-r--r--t/template_tiny/samples/01_hello.txt6
-rw-r--r--t/template_tiny/samples/01_hello.var3
-rw-r--r--t/template_tiny/samples/02_null.tt1
-rw-r--r--t/template_tiny/samples/02_null.txt1
-rw-r--r--t/template_tiny/samples/02_null.var3
-rw-r--r--t/template_tiny/samples/03_chomp.tt16
-rw-r--r--t/template_tiny/samples/03_chomp.txt12
-rw-r--r--t/template_tiny/samples/03_chomp.var1
-rw-r--r--t/template_tiny/samples/04_nested.tt9
-rw-r--r--t/template_tiny/samples/04_nested.txt9
-rw-r--r--t/template_tiny/samples/04_nested.var14
-rw-r--r--t/template_tiny/samples/05_condition.tt14
-rw-r--r--t/template_tiny/samples/05_condition.txt6
-rw-r--r--t/template_tiny/samples/05_condition.var4
-rw-r--r--t/template_tiny/samples/06_object.tt5
-rw-r--r--t/template_tiny/samples/06_object.txt5
-rw-r--r--t/template_tiny/samples/06_object.var5
-rw-r--r--t/template_tiny/samples/07_nesting.tt20
-rw-r--r--t/template_tiny/samples/07_nesting.txt13
-rw-r--r--t/template_tiny/samples/07_nesting.var4
-rw-r--r--t/template_tiny/samples/08_foreach.tt11
-rw-r--r--t/template_tiny/samples/08_foreach.txt6
-rw-r--r--t/template_tiny/samples/08_foreach.var15
-rw-r--r--t/template_tiny/samples/09_trim.tt2
-rw-r--r--t/template_tiny/samples/09_trim.txt1
-rw-r--r--t/template_tiny/samples/09_trim.var2
-rw-r--r--t/time.t64
-rw-r--r--t/types.t225
-rw-r--r--t/uri_for.t30
-rw-r--r--t/vars.t35
-rw-r--r--t/views/auto_page.tt1
-rw-r--r--t/views/beforetemplate.tt1
-rw-r--r--t/views/folder/page.tt1
-rw-r--r--t/views/index.tt5
-rw-r--r--t/views/layouts/main.tt7
-rw-r--r--t/views/session_in_template.tt1
-rw-r--r--t/views/template_simple_index.tt7
-rw-r--r--t/views/tokens.tt6
-rw-r--r--xt/perlcritic.rc60
-rw-r--r--xt/perltidy.rc33
-rw-r--r--xt/whitespace.t16
385 files changed, 43980 insertions, 0 deletions
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 00000000..1f28a349
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,2 @@
+See perldoc Dancer2.pm, section AUTHORS, for a list of core developers and
+contributors.
diff --git a/Changes b/Changes
new file mode 100644
index 00000000..9d25b763
--- /dev/null
+++ b/Changes
@@ -0,0 +1,1671 @@
+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).
diff --git a/GitGuide.md b/GitGuide.md
new file mode 100644
index 00000000..4088b79a
--- /dev/null
+++ b/GitGuide.md
@@ -0,0 +1,270 @@
+# Git Guide
+
+This guide will help you to set up your environment to be able to work
+on the Dancer2's repository.
+
+## Contributing
+
+This guide has been written to help anyone interested in contributing
+to the development of Dancer2.
+
+First of all - thank you for your interest in the project! It's the
+community of helpful contributors who've helped Dancer grow
+phenomenally. Without the community we wouldn't be where we are today!
+
+Please read this guide before contributing to Dancer2, to avoid wasted
+effort and maximizing the chances of your contributions being used.
+
+There are many ways to contribute to the project. Dancer2 is a young
+yet active project and any kind of help is very much appreciated!
+
+### Documentation
+
+We value documentation very much, but it's difficult to keep it
+up-to-date. If you find a typo or an error in the documentation
+please do let us know - ideally by submitting a patch (pull request)
+with your fix or suggestion (see
+[Patch Submission](#environment-and-patch-submission)).
+
+### Code
+
+You can write extensions (plugins) for Dancer2 extending core
+functionality or contribute to Dancer2's core code, see
+[Patch Submission](#environment-and-patch-submission) below.
+
+## General Development Guidelines
+
+This section lists high-level recommendations for developing Dancer2,
+for more detailed guidelines, see [Coding Guidelines](#coding-guidelines)
+below.
+
+### Quality Assurance
+
+Dancer2 should be able to install for all Perl versions since 5.8, on
+any platform for which Perl exists. We focus mainly on GNU/Linux (any
+distribution), \*BSD and Windows (native and Cygwin).
+
+We should avoid regressions as much as possible and keep backwards
+compatibility in mind when refactoring. Stable releases should not
+break functionality and new releases should provide an upgrade path
+and upgrade tips such as warning the user about deprecated
+functionality.
+
+### Quality Supervision
+
+We can measure our quality using the
+[CPAN testers platform](http://www.cpantesters.org).
+
+A good way to help the project is to find a failing build log on the
+[CPAN testers](http://www.cpantesters.org/distro/D/Dancer2.html).
+
+If you find a failing test report, feel free to report it as a
+[GitHub issue](http://github.com/PerlDancer/Dancer2/issues).
+
+### Reporting Bugs
+
+We prefer to have all our bug reports on GitHub, in the
+[issues section](http://github.com/PerlDancer/Dancer2/issues).
+
+Please make sure the bug you're reporting does not yet exist. If in doubt
+please ask on IRC.
+
+## Environment and Patch Submission
+
+### Set up a development environment
+
+If you want to submit a patch for Dancer2, you need git and very
+likely also [_Dist::Zilla_](https://metacpan.org/module/Dist::Zilla).
+We also recommend perlbrew (see below) or,
+alternatively, [_App::Plenv_](https://github.com/tokuhirom/plenv))
+to test and develop Dancer2 on a recent
+version of Perl. We also suggest
+[_App::cpanminus_](https://metacpan.org/module/App::cpanminus)
+to quickly and comfortably install Perl modules.
+
+In the following sections we provide tips for the installation of some
+of these tools together with Dancer. Please also see the documentation
+that comes with these tools for more info.
+
+#### Perlbrew tips (Optional)
+
+Install perlbrew for example with
+
+ $ cpanm App::perlbrew
+
+Check which Perls are available
+
+ $ perlbrew available
+
+It should list the available Perl versions, like this (incomplete) list:
+
+ perl-5.17.1
+ perl-5.16.0
+ perl-5.14.2
+ perl-5.12.4
+ ...
+
+Now run the init command for perlbrew. The init command initializes and controls
+processes. The init command is run as the last step of any startup
+process.
+
+ $ perlbrew init
+
+
+Then install a version inside perlbrew. We recommend you
+give a name to the installation (`--as` option), as well as compiling
+without the tests (`--n` option) to speed it up.
+
+ $ perlbrew install -n perl-5.14.4 --as dancer_development -j 3
+
+Wait a while, and it should be done. Switch to your new Perl with:
+
+ $ perlbrew switch dancer_development
+
+Now you are using the fresh Perl, you can check it with:
+
+ $ which perl
+
+Install cpanm on your brewed version of Perl.
+
+ $ perlbrew install-cpanm
+
+
+### Install various dependencies (required)
+
+Install Dist::Zilla
+
+ $ cpanm Dist::Zilla
+
+### Get Dancer2 sources
+
+Get the Dancer sources from github (for a more complete git workflow
+see below):
+
+Clone your fork to have a local copy using the following command:
+
+ $ git clone git://github.com/perldancer/Dancer2.git
+
+The Dancer2 sources come with a `dist.ini`. That's the configuration
+file for _Dist::Zilla_, so that it knows how to build Dancer2. Let's
+use dist.ini to install additional `Dist::Zilla` plugins which are
+not yet installed on your system (or Perl installation):
+
+ $ dzil authordeps | cpanm -n
+
+That should install a bunch of stuff. Now that _Dist::Zilla_ is up and
+running, you should install the dependencies required by Dancer2:
+
+ $ dzil listdeps | cpanm -n
+
+When that is done, you're good to go! You can use `dzil` to build and test
+Dancer2:
+
+ $ dzil build
+ $ dzil test --no-author
+
+
+### Running your modified version
+
+If you have any version of Dancer2 installed on your system you will likely
+run into problems when you try and run the "Dancer2" binary due to the wrong
+lib's being used.
+The following command should resolve that.
+```bash
+perl -Ilib script/dancer2 gen -s share/skel --overwrite --path /tmp/d2app -a MyApp::App
+```
+- It assumes we are in the git repo root dir
+- `-Ilib` - tells perl to include the lib dir in it's search path
+- in this case we run "gen" and
+ - `-s share/skel` - specify the use of the local copy of the skel dir
+ - `--overwrite` - we want to overwrite the generated scaffold project
+ - `--path /tmp/d2app` - the dir to write the generated scaffold project dir to
+ - `-a MyApp::App` - the name of the app project we want to generate
+
+
+### Patch Submission (Github workflow)
+
+The Dancer2 development team uses GitHub to collaborate. We greatly
+appreciate contributions submitted via GitHub, as it makes tracking
+these contributions and applying them much, much easier. This gives
+your contribution a much better chance of being integrated into
+Dancer2 quickly!
+
+**NOTE:** All active development is performed in the _master_ branch.
+Therefore, all your contribution work should be done in a fork of the
+_master_ branch.
+
+Here is the workflow for submitting a patch:
+
+1. Fork the repository: http://github.com/PerlDancer/Dancer2 and click "Fork";
+
+2. Clone your fork to have a local copy using the following command:
+
+ $ git clone git://github.com/myname/Dancer2.git
+
+3. As a contributor, you should **always** work on the `master` branch of
+ your clone.
+
+ $ git remote add upstream https://github.com/PerlDancer/Dancer2.git
+ $ git fetch upstream
+
+ This will create a local branch in your clone named _master_ and
+ that will track the official _master_ branch. That way, if you have
+ more or less commits than the upstream repo, you'll be immediately
+ notified by git.
+
+4. You want to isolate all your commits in a _topic_ branch, this
+ will make the reviewing much easier for the core team and will
+ allow you to continue working on your clone without worrying about
+ different commits mixing together.
+
+ To do that, first create a local branch to build your pull request:
+
+ # you should be in master here
+ $ git checkout -b pr/$name
+
+ Now you have created a local branch named _pr/$name_ where _$name_
+ is the name you want (it should describe the purpose of the pull
+ request you're preparing).
+
+ In that branch, do all the commits you need (the more the better)
+ and when done, push the branch to your fork:
+
+ # ... commits ...
+ git push origin pr/$name
+
+ You are now ready to send a pull request.
+
+5. Send a _pull request_ via the GitHub interface. Make sure your pull
+ request is based on the _pr/$name_ branch you've just pushed, so
+ that it incorporates the appropriate commits only.
+
+ It's also a good idea to summarize your work in a report sent to
+ the users' mailing list (see below), in order to make sure the team
+ is aware of it.
+
+ You could also notify the core team on IRC, on `irc.perl.org`,
+ channel `#dancer` or via [web client](http://www.perldancer.org/irc).
+
+6. When the core team reviews your pull request, it will either accept
+ (and then merge into _master_) or refuse your request.
+
+ If it's refused, try to understand the reasons explained by the
+ team for the denial. Most of the time, communicating with the core
+ team is enough to understand what the mistake was. Above all,
+ please don't be offended.
+
+ If your pull request is merged into _master_, then all you have to
+ do is remove your local and remote _pr/$name_ branch:
+
+ $ git checkout master
+ $ git branch -D pr/$name
+ $ git push origin :pr/$name
+
+ And then, of course, you need to sync your local devel branch with
+ upstream:
+
+ $ git pull upstream master
+ $ git push origin master
+
+ You're now ready to start working on a new pull request!
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..b370564a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,379 @@
+This software is copyright (c) 2019 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) 2019 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) 19yy <name of author>
+
+ 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.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
+--- The Artistic License 1.0 ---
+
+This software is Copyright (c) 2019 by Alexis Sukrieh.
+
+This is free software, licensed under:
+
+ The Artistic License 1.0
+
+The Artistic License
+
+Preamble
+
+The intent of this document is to state the conditions under which a Package
+may be copied, such that the Copyright Holder maintains some semblance of
+artistic control over the development of the package, while giving the users of
+the package the right to use and distribute the Package in a more-or-less
+customary fashion, plus the right to make reasonable modifications.
+
+Definitions:
+
+ - "Package" refers to the collection of files distributed by the Copyright
+ Holder, and derivatives of that collection of files created through
+ textual modification.
+ - "Standard Version" refers to such a Package if it has not been modified,
+ or has been modified in accordance with the wishes of the Copyright
+ Holder.
+ - "Copyright Holder" is whoever is named in the copyright or copyrights for
+ the package.
+ - "You" is you, if you're thinking about copying or distributing this Package.
+ - "Reasonable copying fee" is whatever you can justify on the basis of media
+ cost, duplication charges, time of people involved, and so on. (You will
+ not be required to justify it to the Copyright Holder, but only to the
+ computing community at large as a market that must bear the fee.)
+ - "Freely Available" means that no fee is charged for the item itself, though
+ there may be fees involved in handling the item. It also means that
+ recipients of the item may redistribute it under the same conditions they
+ received it.
+
+1. You may make and give away verbatim copies of the source form of the
+Standard Version of this Package without restriction, provided that you
+duplicate all of the original copyright notices and associated disclaimers.
+
+2. You may apply bug fixes, portability fixes and other modifications derived
+from the Public Domain or from the Copyright Holder. A Package modified in such
+a way shall still be considered the Standard Version.
+
+3. You may otherwise modify your copy of this Package in any way, provided that
+you insert a prominent notice in each changed file stating how and when you
+changed that file, and provided that you do at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise make them
+ Freely Available, such as by posting said modifications to Usenet or an
+ equivalent medium, or placing the modifications on a major archive site
+ such as ftp.uu.net, or by allowing the Copyright Holder to include your
+ modifications in the Standard Version of the Package.
+
+ b) use the modified Package only within your corporation or organization.
+
+ c) rename any non-standard executables so the names do not conflict with
+ standard executables, which must also be provided, and provide a separate
+ manual page for each non-standard executable that clearly documents how it
+ differs from the Standard Version.
+
+ d) make other distribution arrangements with the Copyright Holder.
+
+4. You may distribute the programs of this Package in object code or executable
+form, provided that you do at least ONE of the following:
+
+ a) distribute a Standard Version of the executables and library files,
+ together with instructions (in the manual page or equivalent) on where to
+ get the Standard Version.
+
+ b) accompany the distribution with the machine-readable source of the Package
+ with your modifications.
+
+ c) accompany any non-standard executables with their corresponding Standard
+ Version executables, giving the non-standard executables non-standard
+ names, and clearly documenting the differences in manual pages (or
+ equivalent), together with instructions on where to get the Standard
+ Version.
+
+ d) make other distribution arrangements with the Copyright Holder.
+
+5. You may charge a reasonable copying fee for any distribution of this
+Package. You may charge any fee you choose for support of this Package. You
+may not charge a fee for this Package itself. However, you may distribute this
+Package in aggregate with other (possibly commercial) programs as part of a
+larger (possibly commercial) software distribution provided that you do not
+advertise this Package as a product of your own.
+
+6. The scripts and library files supplied as input to or produced as output
+from the programs of this Package do not automatically fall under the copyright
+of this Package, but belong to whomever generated them, and may be sold
+commercially, and may be aggregated with this Package.
+
+7. C or perl subroutines supplied by you and linked into this Package shall not
+be considered part of this Package.
+
+8. The name of the Copyright Holder may not be used to endorse or promote
+products derived from this software without specific prior written permission.
+
+9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
+MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+
+The End
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 00000000..92e2a319
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,387 @@
+# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.012.
+AUTHORS
+Changes
+GitGuide.md
+LICENSE
+MANIFEST
+META.json
+META.yml
+Makefile.PL
+cpanfile
+examples/single/hello_world.psgi
+examples/single/simple_calculator.psgi
+lib/Dancer2.pm
+lib/Dancer2/CLI.pm
+lib/Dancer2/CLI/Command/gen.pm
+lib/Dancer2/CLI/Command/version.pm
+lib/Dancer2/Config.pod
+lib/Dancer2/Cookbook.pod
+lib/Dancer2/Core.pm
+lib/Dancer2/Core/App.pm
+lib/Dancer2/Core/Cookie.pm
+lib/Dancer2/Core/DSL.pm
+lib/Dancer2/Core/Dispatcher.pm
+lib/Dancer2/Core/Error.pm
+lib/Dancer2/Core/Factory.pm
+lib/Dancer2/Core/HTTP.pm
+lib/Dancer2/Core/Hook.pm
+lib/Dancer2/Core/MIME.pm
+lib/Dancer2/Core/Request.pm
+lib/Dancer2/Core/Request/Upload.pm
+lib/Dancer2/Core/Response.pm
+lib/Dancer2/Core/Response/Delayed.pm
+lib/Dancer2/Core/Role/ConfigReader.pm
+lib/Dancer2/Core/Role/DSL.pm
+lib/Dancer2/Core/Role/Engine.pm
+lib/Dancer2/Core/Role/Handler.pm
+lib/Dancer2/Core/Role/HasLocation.pm
+lib/Dancer2/Core/Role/Hookable.pm
+lib/Dancer2/Core/Role/Logger.pm
+lib/Dancer2/Core/Role/Serializer.pm
+lib/Dancer2/Core/Role/SessionFactory.pm
+lib/Dancer2/Core/Role/SessionFactory/File.pm
+lib/Dancer2/Core/Role/StandardResponses.pm
+lib/Dancer2/Core/Role/Template.pm
+lib/Dancer2/Core/Route.pm
+lib/Dancer2/Core/Runner.pm
+lib/Dancer2/Core/Session.pm
+lib/Dancer2/Core/Time.pm
+lib/Dancer2/Core/Types.pm
+lib/Dancer2/FileUtils.pm
+lib/Dancer2/Handler/AutoPage.pm
+lib/Dancer2/Handler/File.pm
+lib/Dancer2/Logger/Capture.pm
+lib/Dancer2/Logger/Capture/Trap.pm
+lib/Dancer2/Logger/Console.pm
+lib/Dancer2/Logger/Diag.pm
+lib/Dancer2/Logger/File.pm
+lib/Dancer2/Logger/Note.pm
+lib/Dancer2/Logger/Null.pm
+lib/Dancer2/Manual.pod
+lib/Dancer2/Manual/Deployment.pod
+lib/Dancer2/Manual/Migration.pod
+lib/Dancer2/Manual/Testing.pod
+lib/Dancer2/Plugin.pm
+lib/Dancer2/Plugins.pod
+lib/Dancer2/Policy.pod
+lib/Dancer2/Serializer/Dumper.pm
+lib/Dancer2/Serializer/JSON.pm
+lib/Dancer2/Serializer/Mutable.pm
+lib/Dancer2/Serializer/YAML.pm
+lib/Dancer2/Session/Simple.pm
+lib/Dancer2/Session/YAML.pm
+lib/Dancer2/Template/Implementation/ForkedTiny.pm
+lib/Dancer2/Template/Simple.pm
+lib/Dancer2/Template/TemplateToolkit.pm
+lib/Dancer2/Template/Tiny.pm
+lib/Dancer2/Test.pm
+lib/Dancer2/Tutorial.pod
+script/dancer2
+share/skel/.dancer
+share/skel/MANIFEST.SKIP
+share/skel/Makefile.PL
+share/skel/bin/+app.psgi
+share/skel/config.yml
+share/skel/cpanfile
+share/skel/environments/development.yml
+share/skel/environments/production.yml
+share/skel/lib/AppFile.pm
+share/skel/public/+dispatch.cgi
+share/skel/public/+dispatch.fcgi
+share/skel/public/404.html
+share/skel/public/500.html
+share/skel/public/css/error.css
+share/skel/public/css/style.css
+share/skel/public/favicon.ico
+share/skel/public/images/perldancer-bg.jpg
+share/skel/public/images/perldancer.jpg
+share/skel/public/javascripts/jquery.js
+share/skel/t/001_base.t
+share/skel/t/002_index_route.t
+share/skel/views/index.tt
+share/skel/views/layouts/main.tt
+t/00-compile.t
+t/00-report-prereqs.dd
+t/00-report-prereqs.t
+t/app.t
+t/app/t1/bin/app.psgi
+t/app/t1/config.yml
+t/app/t1/lib/App1.pm
+t/app/t1/lib/Sub/App2.pm
+t/app/t2/.dancer
+t/app/t2/config.yml
+t/app/t2/lib/App3.pm
+t/app_alone.t
+t/author-no-tabs.t
+t/author-pod-syntax.t
+t/auto_page.t
+t/caller.t
+t/charset_server.t
+t/classes/Dancer2-Core-Factory/new.t
+t/classes/Dancer2-Core-Hook/new.t
+t/classes/Dancer2-Core-Request/new.t
+t/classes/Dancer2-Core-Request/serializers.t
+t/classes/Dancer2-Core-Response-Delayed/after_hooks.t
+t/classes/Dancer2-Core-Response-Delayed/new.t
+t/classes/Dancer2-Core-Response/new_from.t
+t/classes/Dancer2-Core-Role-Engine/with.t
+t/classes/Dancer2-Core-Role-Handler/with.t
+t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists
+t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists
+t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer
+t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl
+t/classes/Dancer2-Core-Role-HasLocation/with.t
+t/classes/Dancer2-Core-Role-Serializer/with.t
+t/classes/Dancer2-Core-Role-StandardResponses/with.t
+t/classes/Dancer2-Core-Route/base.t
+t/classes/Dancer2-Core-Route/deprecated_param_keys.t
+t/classes/Dancer2-Core-Route/match.t
+t/classes/Dancer2-Core-Runner/environment.t
+t/classes/Dancer2-Core-Runner/new.t
+t/classes/Dancer2-Core-Runner/psgi_app.t
+t/classes/Dancer2-Core/camelize.t
+t/classes/Dancer2/import-pragmas.t
+t/classes/Dancer2/import.t
+t/config.yml
+t/config/config.yml
+t/config/environments/failure.yml
+t/config/environments/merging.yml
+t/config/environments/production.yml
+t/config/environments/staging.json
+t/config2/config.yml
+t/config2/config_local.yml
+t/config2/environments/lconfig.yml
+t/config2/environments/lconfig_local.yml
+t/config_multiapp.t
+t/config_reader.t
+t/config_settings.t
+t/context-in-before.t
+t/cookie.t
+t/corpus/pretty/505.tt
+t/corpus/pretty/relative.tt
+t/corpus/pretty_public/404.html
+t/corpus/pretty_public/510.html
+t/corpus/static/1x1.png
+t/corpus/static/index.html
+t/custom_dsl.t
+t/dancer-test.t
+t/dancer-test/config.yml
+t/deserialize.t
+t/disp_named_capture.t
+t/dispatcher.t
+t/dsl/any.t
+t/dsl/app.t
+t/dsl/content.t
+t/dsl/delayed.t
+t/dsl/error_template.t
+t/dsl/extend.t
+t/dsl/extend_config/config.yml
+t/dsl/halt.t
+t/dsl/halt_with_param.t
+t/dsl/json.t
+t/dsl/parameters.t
+t/dsl/pass.t
+t/dsl/path.t
+t/dsl/request.t
+t/dsl/route_retvals.t
+t/dsl/send_as.t
+t/dsl/send_file.t
+t/dsl/splat.t
+t/dsl/to_app.t
+t/engine.t
+t/error.t
+t/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-596.t
+t/issues/gh-634.t
+t/issues/gh-639/fails/.dancer
+t/issues/gh-639/fails/config.yml
+t/issues/gh-639/fails/issue.t
+t/issues/gh-639/succeeds/.dancer
+t/issues/gh-639/succeeds/config.yml
+t/issues/gh-639/succeeds/issue.t
+t/issues/gh-650/gh-650.t
+t/issues/gh-650/views/environment_setting.tt
+t/issues/gh-723.t
+t/issues/gh-730.t
+t/issues/gh-762.t
+t/issues/gh-762/views/404.tt
+t/issues/gh-794.t
+t/issues/gh-797.t
+t/issues/gh-799.t
+t/issues/gh-811.t
+t/issues/gh-931.t
+t/issues/gh-936.t
+t/issues/gh-936/views/error.tt
+t/issues/gh-944.t
+t/issues/gh-975/config.yml
+t/issues/gh-975/gh-975.t
+t/issues/gh-975/test_public_dir/test.txt
+t/issues/memleak/die_in_hooks.t
+t/issues/vars-in-forward.t
+t/lib/App1.pm
+t/lib/App2.pm
+t/lib/Dancer2/Plugin/Bar.pm
+t/lib/Dancer2/Plugin/DancerPlugin.pm
+t/lib/Dancer2/Plugin/DefineKeywords.pm
+t/lib/Dancer2/Plugin/EmptyPlugin.pm
+t/lib/Dancer2/Plugin/Foo.pm
+t/lib/Dancer2/Plugin/FooPlugin.pm
+t/lib/Dancer2/Plugin/Hookee.pm
+t/lib/Dancer2/Plugin/OnPluginImport.pm
+t/lib/Dancer2/Plugin/PluginWithImport.pm
+t/lib/Dancer2/Plugin/Polite.pm
+t/lib/Dancer2/Session/SimpleNoChangeId.pm
+t/lib/Foo.pm
+t/lib/MyDancerDSL.pm
+t/lib/PoC/Plugin/Polite.pm
+t/lib/SubApp1.pm
+t/lib/SubApp2.pm
+t/lib/TestApp.pm
+t/lib/TestPod.pm
+t/lib/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/named_apps.t
+t/no_default_middleware.t
+t/plugin2/basic-2.t
+t/plugin2/basic.t
+t/plugin2/define-keywords.t
+t/plugin2/find_plugin.t
+t/plugin2/from-config.t
+t/plugin2/hooks.t
+t/plugin2/inside-plugin.t
+t/plugin2/keywords-hooks-namespace.t
+t/plugin2/memory_cycles.t
+t/plugin2/no-app-munging.t
+t/plugin2/no-clobbering.t
+t/plugin2/no-config.t
+t/plugin2/with-plugins.t
+t/plugin_import.t
+t/plugin_multiple_apps.t
+t/plugin_register.t
+t/plugin_syntax.t
+t/prepare_app.t
+t/psgi_app.t
+t/psgi_app_forward_and_pass.t
+t/public/file.txt
+t/redirect.t
+t/release-distmeta.t
+t/request.t
+t/request_make_forward_to.t
+t/request_upload.t
+t/response.t
+t/roles/hook.t
+t/route-pod-coverage/route-pod-coverage.t
+t/scope_problems/config.yml
+t/scope_problems/dispatcher_internal_request.t
+t/scope_problems/keywords_before_template_hook.t
+t/scope_problems/session_is_cleared.t
+t/scope_problems/views/500.tt
+t/scope_problems/with_return_dies.t
+t/serializer.t
+t/serializer_json.t
+t/serializer_mutable.t
+t/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/shared_engines.t
+t/static_content.t
+t/template.t
+t/template_default_tokens.t
+t/template_ext.t
+t/template_name.t
+t/template_simple.t
+t/template_tiny/01_compile.t
+t/template_tiny/02_trivial.t
+t/template_tiny/03_samples.t
+t/template_tiny/04_compat.t
+t/template_tiny/05_preparse.t
+t/template_tiny/samples/01_hello.tt
+t/template_tiny/samples/01_hello.txt
+t/template_tiny/samples/01_hello.var
+t/template_tiny/samples/02_null.tt
+t/template_tiny/samples/02_null.txt
+t/template_tiny/samples/02_null.var
+t/template_tiny/samples/03_chomp.tt
+t/template_tiny/samples/03_chomp.txt
+t/template_tiny/samples/03_chomp.var
+t/template_tiny/samples/04_nested.tt
+t/template_tiny/samples/04_nested.txt
+t/template_tiny/samples/04_nested.var
+t/template_tiny/samples/05_condition.tt
+t/template_tiny/samples/05_condition.txt
+t/template_tiny/samples/05_condition.var
+t/template_tiny/samples/06_object.tt
+t/template_tiny/samples/06_object.txt
+t/template_tiny/samples/06_object.var
+t/template_tiny/samples/07_nesting.tt
+t/template_tiny/samples/07_nesting.txt
+t/template_tiny/samples/07_nesting.var
+t/template_tiny/samples/08_foreach.tt
+t/template_tiny/samples/08_foreach.txt
+t/template_tiny/samples/08_foreach.var
+t/template_tiny/samples/09_trim.tt
+t/template_tiny/samples/09_trim.txt
+t/template_tiny/samples/09_trim.var
+t/time.t
+t/types.t
+t/uri_for.t
+t/vars.t
+t/views/auto_page.tt
+t/views/beforetemplate.tt
+t/views/folder/page.tt
+t/views/index.tt
+t/views/layouts/main.tt
+t/views/session_in_template.tt
+t/views/template_simple_index.tt
+t/views/tokens.tt
+xt/perlcritic.rc
+xt/perltidy.rc
+xt/whitespace.t
diff --git a/META.json b/META.json
new file mode 100644
index 00000000..a20f19a1
--- /dev/null
+++ b/META.json
@@ -0,0 +1,389 @@
+{
+ "abstract" : "Lightweight yet powerful web application framework",
+ "author" : [
+ "Dancer Core Developers"
+ ],
+ "dynamic_config" : 1,
+ "generated_by" : "Dist::Zilla version 6.012, 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" : {
+ "build" : {
+ "requires" : {
+ "ExtUtils::MakeMaker" : "7.1101",
+ "Test::CPAN::Meta" : "0"
+ }
+ },
+ "configure" : {
+ "requires" : {
+ "CPAN::Meta::Requirements" : "2.120620",
+ "ExtUtils::MakeMaker" : "7.1101",
+ "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" : {
+ "YAML" : "1.16"
+ },
+ "recommends" : {
+ "CGI::Deurl::XS" : "0",
+ "Class::XSAccessor" : "0",
+ "Cpanel::JSON::XS" : "0",
+ "Crypt::URandom" : "0",
+ "HTTP::XSCookies" : "0.000007",
+ "HTTP::XSHeaders" : "0",
+ "Math::Random::ISAAC::XS" : "0",
+ "MooX::TypeTiny" : "0",
+ "Pod::Simple::Search" : "0",
+ "Pod::Simple::SimpleTree" : "0",
+ "Scope::Upper" : "0",
+ "Type::Tiny::XS" : "0",
+ "URL::Encode::XS" : "0",
+ "YAML::XS" : "0"
+ },
+ "requires" : {
+ "App::Cmd::Setup" : "0",
+ "Attribute::Handlers" : "0",
+ "Carp" : "0",
+ "Clone" : "0",
+ "Config::Any" : "0",
+ "Digest::SHA" : "0",
+ "Encode" : "0",
+ "Exporter" : "5.57",
+ "Exporter::Tiny" : "0",
+ "File::Basename" : "0",
+ "File::Copy" : "0",
+ "File::Find" : "0",
+ "File::Path" : "0",
+ "File::Share" : "0",
+ "File::Spec" : "0",
+ "File::Temp" : "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::Runtime" : "0",
+ "Moo" : "2.000000",
+ "Moo::Role" : "0",
+ "POSIX" : "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",
+ "Template" : "0",
+ "Template::Tiny" : "0",
+ "Test::Builder" : "0",
+ "Test::More" : "0.92",
+ "Type::Tiny" : "1.000006",
+ "Types::Standard" : "0",
+ "URI::Escape" : "0",
+ "YAML" : "0.86",
+ "parent" : "0"
+ },
+ "suggests" : {
+ "Fcntl" : "0",
+ "MIME::Types" : "0"
+ }
+ },
+ "test" : {
+ "recommends" : {
+ "CPAN::Meta" : "2.120900"
+ },
+ "requires" : {
+ "Capture::Tiny" : "0.12",
+ "ExtUtils::MakeMaker" : "7.1101",
+ "File::Spec" : "0",
+ "HTTP::Cookies" : "0",
+ "HTTP::Headers" : "0",
+ "IO::Handle" : "0",
+ "IPC::Open3" : "0",
+ "Template" : "0",
+ "Test::Builder" : "0",
+ "Test::EOL" : "0",
+ "Test::Fatal" : "0",
+ "Test::More" : "0.92"
+ }
+ }
+ },
+ "provides" : {
+ "Dancer2" : {
+ "file" : "lib/Dancer2.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::CLI" : {
+ "file" : "lib/Dancer2/CLI.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::CLI::Command::gen" : {
+ "file" : "lib/Dancer2/CLI/Command/gen.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::CLI::Command::version" : {
+ "file" : "lib/Dancer2/CLI/Command/version.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core" : {
+ "file" : "lib/Dancer2/Core.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::App" : {
+ "file" : "lib/Dancer2/Core/App.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Cookie" : {
+ "file" : "lib/Dancer2/Core/Cookie.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::DSL" : {
+ "file" : "lib/Dancer2/Core/DSL.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Dispatcher" : {
+ "file" : "lib/Dancer2/Core/Dispatcher.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Error" : {
+ "file" : "lib/Dancer2/Core/Error.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Factory" : {
+ "file" : "lib/Dancer2/Core/Factory.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::HTTP" : {
+ "file" : "lib/Dancer2/Core/HTTP.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Hook" : {
+ "file" : "lib/Dancer2/Core/Hook.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::MIME" : {
+ "file" : "lib/Dancer2/Core/MIME.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Request" : {
+ "file" : "lib/Dancer2/Core/Request.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Request::Upload" : {
+ "file" : "lib/Dancer2/Core/Request/Upload.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Response" : {
+ "file" : "lib/Dancer2/Core/Response.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Response::Delayed" : {
+ "file" : "lib/Dancer2/Core/Response/Delayed.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::ConfigReader" : {
+ "file" : "lib/Dancer2/Core/Role/ConfigReader.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::DSL" : {
+ "file" : "lib/Dancer2/Core/Role/DSL.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::Engine" : {
+ "file" : "lib/Dancer2/Core/Role/Engine.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::Handler" : {
+ "file" : "lib/Dancer2/Core/Role/Handler.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::HasLocation" : {
+ "file" : "lib/Dancer2/Core/Role/HasLocation.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::Hookable" : {
+ "file" : "lib/Dancer2/Core/Role/Hookable.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::Logger" : {
+ "file" : "lib/Dancer2/Core/Role/Logger.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::Serializer" : {
+ "file" : "lib/Dancer2/Core/Role/Serializer.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::SessionFactory" : {
+ "file" : "lib/Dancer2/Core/Role/SessionFactory.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::SessionFactory::File" : {
+ "file" : "lib/Dancer2/Core/Role/SessionFactory/File.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::StandardResponses" : {
+ "file" : "lib/Dancer2/Core/Role/StandardResponses.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Role::Template" : {
+ "file" : "lib/Dancer2/Core/Role/Template.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Route" : {
+ "file" : "lib/Dancer2/Core/Route.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Runner" : {
+ "file" : "lib/Dancer2/Core/Runner.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Session" : {
+ "file" : "lib/Dancer2/Core/Session.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Time" : {
+ "file" : "lib/Dancer2/Core/Time.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Core::Types" : {
+ "file" : "lib/Dancer2/Core/Types.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::FileUtils" : {
+ "file" : "lib/Dancer2/FileUtils.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Handler::AutoPage" : {
+ "file" : "lib/Dancer2/Handler/AutoPage.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Handler::File" : {
+ "file" : "lib/Dancer2/Handler/File.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Logger::Capture" : {
+ "file" : "lib/Dancer2/Logger/Capture.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Logger::Capture::Trap" : {
+ "file" : "lib/Dancer2/Logger/Capture/Trap.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Logger::Console" : {
+ "file" : "lib/Dancer2/Logger/Console.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Logger::Diag" : {
+ "file" : "lib/Dancer2/Logger/Diag.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Logger::File" : {
+ "file" : "lib/Dancer2/Logger/File.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Logger::Note" : {
+ "file" : "lib/Dancer2/Logger/Note.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Logger::Null" : {
+ "file" : "lib/Dancer2/Logger/Null.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Plugin" : {
+ "file" : "lib/Dancer2/Plugin.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Serializer::Dumper" : {
+ "file" : "lib/Dancer2/Serializer/Dumper.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Serializer::JSON" : {
+ "file" : "lib/Dancer2/Serializer/JSON.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Serializer::Mutable" : {
+ "file" : "lib/Dancer2/Serializer/Mutable.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Serializer::YAML" : {
+ "file" : "lib/Dancer2/Serializer/YAML.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Session::Simple" : {
+ "file" : "lib/Dancer2/Session/Simple.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Session::YAML" : {
+ "file" : "lib/Dancer2/Session/YAML.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Template::Implementation::ForkedTiny" : {
+ "file" : "lib/Dancer2/Template/Implementation/ForkedTiny.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Template::Simple" : {
+ "file" : "lib/Dancer2/Template/Simple.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Template::TemplateToolkit" : {
+ "file" : "lib/Dancer2/Template/TemplateToolkit.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Template::Tiny" : {
+ "file" : "lib/Dancer2/Template/Tiny.pm",
+ "version" : "0.300000"
+ },
+ "Dancer2::Test" : {
+ "file" : "lib/Dancer2/Test.pm",
+ "version" : "0.300000"
+ }
+ },
+ "release_status" : "stable",
+ "resources" : {
+ "bugtracker" : {
+ "web" : "https://github.com/PerlDancer/Dancer2/issues"
+ },
+ "homepage" : "http://perldancer.org/",
+ "repository" : {
+ "type" : "git",
+ "url" : "git://github.com/PerlDancer/Dancer2.git",
+ "web" : "https://github.com/PerlDancer/Dancer2"
+ },
+ "x_IRC" : "irc://irc.perl.org/#dancer",
+ "x_WebIRC" : "https://chat.mibbit.com/#dancer@irc.perl.org"
+ },
+ "version" : "0.300000",
+ "x_generated_by_perl" : "v5.28.2",
+ "x_serialization_backend" : "Cpanel::JSON::XS version 4.18"
+}
+
diff --git a/META.yml b/META.yml
new file mode 100644
index 00000000..d60d9c73
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,274 @@
+---
+abstract: 'Lightweight yet powerful web application framework'
+author:
+ - 'Dancer Core Developers'
+build_requires:
+ Capture::Tiny: '0.12'
+ ExtUtils::MakeMaker: '7.1101'
+ File::Spec: '0'
+ HTTP::Cookies: '0'
+ HTTP::Headers: '0'
+ IO::Handle: '0'
+ IPC::Open3: '0'
+ Template: '0'
+ Test::Builder: '0'
+ Test::CPAN::Meta: '0'
+ Test::EOL: '0'
+ Test::Fatal: '0'
+ Test::More: '0.92'
+configure_requires:
+ CPAN::Meta::Requirements: '2.120620'
+ ExtUtils::MakeMaker: '7.1101'
+ File::ShareDir::Install: '0.06'
+ Module::Metadata: '0'
+conflicts:
+ YAML: '1.16'
+dynamic_config: 1
+generated_by: 'Dist::Zilla version 6.012, 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: '0.300000'
+ Dancer2::CLI:
+ file: lib/Dancer2/CLI.pm
+ version: '0.300000'
+ Dancer2::CLI::Command::gen:
+ file: lib/Dancer2/CLI/Command/gen.pm
+ version: '0.300000'
+ Dancer2::CLI::Command::version:
+ file: lib/Dancer2/CLI/Command/version.pm
+ version: '0.300000'
+ Dancer2::Core:
+ file: lib/Dancer2/Core.pm
+ version: '0.300000'
+ Dancer2::Core::App:
+ file: lib/Dancer2/Core/App.pm
+ version: '0.300000'
+ Dancer2::Core::Cookie:
+ file: lib/Dancer2/Core/Cookie.pm
+ version: '0.300000'
+ Dancer2::Core::DSL:
+ file: lib/Dancer2/Core/DSL.pm
+ version: '0.300000'
+ Dancer2::Core::Dispatcher:
+ file: lib/Dancer2/Core/Dispatcher.pm
+ version: '0.300000'
+ Dancer2::Core::Error:
+ file: lib/Dancer2/Core/Error.pm
+ version: '0.300000'
+ Dancer2::Core::Factory:
+ file: lib/Dancer2/Core/Factory.pm
+ version: '0.300000'
+ Dancer2::Core::HTTP:
+ file: lib/Dancer2/Core/HTTP.pm
+ version: '0.300000'
+ Dancer2::Core::Hook:
+ file: lib/Dancer2/Core/Hook.pm
+ version: '0.300000'
+ Dancer2::Core::MIME:
+ file: lib/Dancer2/Core/MIME.pm
+ version: '0.300000'
+ Dancer2::Core::Request:
+ file: lib/Dancer2/Core/Request.pm
+ version: '0.300000'
+ Dancer2::Core::Request::Upload:
+ file: lib/Dancer2/Core/Request/Upload.pm
+ version: '0.300000'
+ Dancer2::Core::Response:
+ file: lib/Dancer2/Core/Response.pm
+ version: '0.300000'
+ Dancer2::Core::Response::Delayed:
+ file: lib/Dancer2/Core/Response/Delayed.pm
+ version: '0.300000'
+ Dancer2::Core::Role::ConfigReader:
+ file: lib/Dancer2/Core/Role/ConfigReader.pm
+ version: '0.300000'
+ Dancer2::Core::Role::DSL:
+ file: lib/Dancer2/Core/Role/DSL.pm
+ version: '0.300000'
+ Dancer2::Core::Role::Engine:
+ file: lib/Dancer2/Core/Role/Engine.pm
+ version: '0.300000'
+ Dancer2::Core::Role::Handler:
+ file: lib/Dancer2/Core/Role/Handler.pm
+ version: '0.300000'
+ Dancer2::Core::Role::HasLocation:
+ file: lib/Dancer2/Core/Role/HasLocation.pm
+ version: '0.300000'
+ Dancer2::Core::Role::Hookable:
+ file: lib/Dancer2/Core/Role/Hookable.pm
+ version: '0.300000'
+ Dancer2::Core::Role::Logger:
+ file: lib/Dancer2/Core/Role/Logger.pm
+ version: '0.300000'
+ Dancer2::Core::Role::Serializer:
+ file: lib/Dancer2/Core/Role/Serializer.pm
+ version: '0.300000'
+ Dancer2::Core::Role::SessionFactory:
+ file: lib/Dancer2/Core/Role/SessionFactory.pm
+ version: '0.300000'
+ Dancer2::Core::Role::SessionFactory::File:
+ file: lib/Dancer2/Core/Role/SessionFactory/File.pm
+ version: '0.300000'
+ Dancer2::Core::Role::StandardResponses:
+ file: lib/Dancer2/Core/Role/StandardResponses.pm
+ version: '0.300000'
+ Dancer2::Core::Role::Template:
+ file: lib/Dancer2/Core/Role/Template.pm
+ version: '0.300000'
+ Dancer2::Core::Route:
+ file: lib/Dancer2/Core/Route.pm
+ version: '0.300000'
+ Dancer2::Core::Runner:
+ file: lib/Dancer2/Core/Runner.pm
+ version: '0.300000'
+ Dancer2::Core::Session:
+ file: lib/Dancer2/Core/Session.pm
+ version: '0.300000'
+ Dancer2::Core::Time:
+ file: lib/Dancer2/Core/Time.pm
+ version: '0.300000'
+ Dancer2::Core::Types:
+ file: lib/Dancer2/Core/Types.pm
+ version: '0.300000'
+ Dancer2::FileUtils:
+ file: lib/Dancer2/FileUtils.pm
+ version: '0.300000'
+ Dancer2::Handler::AutoPage:
+ file: lib/Dancer2/Handler/AutoPage.pm
+ version: '0.300000'
+ Dancer2::Handler::File:
+ file: lib/Dancer2/Handler/File.pm
+ version: '0.300000'
+ Dancer2::Logger::Capture:
+ file: lib/Dancer2/Logger/Capture.pm
+ version: '0.300000'
+ Dancer2::Logger::Capture::Trap:
+ file: lib/Dancer2/Logger/Capture/Trap.pm
+ version: '0.300000'
+ Dancer2::Logger::Console:
+ file: lib/Dancer2/Logger/Console.pm
+ version: '0.300000'
+ Dancer2::Logger::Diag:
+ file: lib/Dancer2/Logger/Diag.pm
+ version: '0.300000'
+ Dancer2::Logger::File:
+ file: lib/Dancer2/Logger/File.pm
+ version: '0.300000'
+ Dancer2::Logger::Note:
+ file: lib/Dancer2/Logger/Note.pm
+ version: '0.300000'
+ Dancer2::Logger::Null:
+ file: lib/Dancer2/Logger/Null.pm
+ version: '0.300000'
+ Dancer2::Plugin:
+ file: lib/Dancer2/Plugin.pm
+ version: '0.300000'
+ Dancer2::Serializer::Dumper:
+ file: lib/Dancer2/Serializer/Dumper.pm
+ version: '0.300000'
+ Dancer2::Serializer::JSON:
+ file: lib/Dancer2/Serializer/JSON.pm
+ version: '0.300000'
+ Dancer2::Serializer::Mutable:
+ file: lib/Dancer2/Serializer/Mutable.pm
+ version: '0.300000'
+ Dancer2::Serializer::YAML:
+ file: lib/Dancer2/Serializer/YAML.pm
+ version: '0.300000'
+ Dancer2::Session::Simple:
+ file: lib/Dancer2/Session/Simple.pm
+ version: '0.300000'
+ Dancer2::Session::YAML:
+ file: lib/Dancer2/Session/YAML.pm
+ version: '0.300000'
+ Dancer2::Template::Implementation::ForkedTiny:
+ file: lib/Dancer2/Template/Implementation/ForkedTiny.pm
+ version: '0.300000'
+ Dancer2::Template::Simple:
+ file: lib/Dancer2/Template/Simple.pm
+ version: '0.300000'
+ Dancer2::Template::TemplateToolkit:
+ file: lib/Dancer2/Template/TemplateToolkit.pm
+ version: '0.300000'
+ Dancer2::Template::Tiny:
+ file: lib/Dancer2/Template/Tiny.pm
+ version: '0.300000'
+ Dancer2::Test:
+ file: lib/Dancer2/Test.pm
+ version: '0.300000'
+recommends:
+ CGI::Deurl::XS: '0'
+ Class::XSAccessor: '0'
+ Cpanel::JSON::XS: '0'
+ Crypt::URandom: '0'
+ HTTP::XSCookies: '0.000007'
+ HTTP::XSHeaders: '0'
+ Math::Random::ISAAC::XS: '0'
+ MooX::TypeTiny: '0'
+ Pod::Simple::Search: '0'
+ Pod::Simple::SimpleTree: '0'
+ Scope::Upper: '0'
+ Type::Tiny::XS: '0'
+ URL::Encode::XS: '0'
+ YAML::XS: '0'
+requires:
+ App::Cmd::Setup: '0'
+ Attribute::Handlers: '0'
+ Carp: '0'
+ Clone: '0'
+ Config::Any: '0'
+ Digest::SHA: '0'
+ Encode: '0'
+ Exporter: '5.57'
+ Exporter::Tiny: '0'
+ File::Basename: '0'
+ File::Copy: '0'
+ File::Find: '0'
+ File::Path: '0'
+ File::Share: '0'
+ File::Spec: '0'
+ File::Temp: '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::Runtime: '0'
+ Moo: '2.000000'
+ Moo::Role: '0'
+ POSIX: '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'
+ Template: '0'
+ Template::Tiny: '0'
+ Test::Builder: '0'
+ Test::More: '0.92'
+ Type::Tiny: '1.000006'
+ Types::Standard: '0'
+ URI::Escape: '0'
+ YAML: '0.86'
+ parent: '0'
+resources:
+ IRC: irc://irc.perl.org/#dancer
+ WebIRC: https://chat.mibbit.com/#dancer@irc.perl.org
+ bugtracker: https://github.com/PerlDancer/Dancer2/issues
+ homepage: http://perldancer.org/
+ repository: git://github.com/PerlDancer/Dancer2.git
+version: '0.300000'
+x_generated_by_perl: v5.28.2
+x_serialization_backend: 'YAML::Tiny version 1.73'
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 00000000..ab202b9f
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,217 @@
+# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.012.
+use strict;
+use warnings;
+
+
+
+use ExtUtils::MakeMaker 7.1101;
+
+use File::ShareDir::Install;
+$File::ShareDir::Install::INCLUDE_DOTFILES = 1;
+$File::ShareDir::Install::INCLUDE_DOTDIRS = 1;
+install_share dist => "share";
+
+
+my %WriteMakefileArgs = (
+ "ABSTRACT" => "Lightweight yet powerful web application framework",
+ "AUTHOR" => "Dancer Core Developers",
+ "BUILD_REQUIRES" => {
+ "ExtUtils::MakeMaker" => "7.1101",
+ "Test::CPAN::Meta" => 0
+ },
+ "CONFIGURE_REQUIRES" => {
+ "CPAN::Meta::Requirements" => "2.120620",
+ "ExtUtils::MakeMaker" => "7.1101",
+ "File::ShareDir::Install" => "0.06",
+ "Module::Metadata" => 0
+ },
+ "DISTNAME" => "Dancer2",
+ "EXE_FILES" => [
+ "script/dancer2"
+ ],
+ "LICENSE" => "perl",
+ "NAME" => "Dancer2",
+ "PREREQ_PM" => {
+ "App::Cmd::Setup" => 0,
+ "Attribute::Handlers" => 0,
+ "Carp" => 0,
+ "Clone" => 0,
+ "Config::Any" => 0,
+ "Digest::SHA" => 0,
+ "Encode" => 0,
+ "Exporter" => "5.57",
+ "Exporter::Tiny" => 0,
+ "File::Basename" => 0,
+ "File::Copy" => 0,
+ "File::Find" => 0,
+ "File::Path" => 0,
+ "File::Share" => 0,
+ "File::Spec" => 0,
+ "File::Temp" => 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::Runtime" => 0,
+ "Moo" => "2.000000",
+ "Moo::Role" => 0,
+ "POSIX" => 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,
+ "Template" => 0,
+ "Template::Tiny" => 0,
+ "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" => "7.1101",
+ "File::Spec" => 0,
+ "HTTP::Cookies" => 0,
+ "HTTP::Headers" => 0,
+ "IO::Handle" => 0,
+ "IPC::Open3" => 0,
+ "Template" => 0,
+ "Test::Builder" => 0,
+ "Test::EOL" => 0,
+ "Test::Fatal" => 0,
+ "Test::More" => "0.92"
+ },
+ "VERSION" => "0.300000",
+ "test" => {
+ "TESTS" => "t/*.t t/classes/Dancer2-Core-Factory/*.t t/classes/Dancer2-Core-Hook/*.t t/classes/Dancer2-Core-Request/*.t t/classes/Dancer2-Core-Response-Delayed/*.t t/classes/Dancer2-Core-Response/*.t t/classes/Dancer2-Core-Role-Engine/*.t t/classes/Dancer2-Core-Role-Handler/*.t t/classes/Dancer2-Core-Role-HasLocation/*.t t/classes/Dancer2-Core-Role-Serializer/*.t t/classes/Dancer2-Core-Role-StandardResponses/*.t t/classes/Dancer2-Core-Route/*.t t/classes/Dancer2-Core-Runner/*.t t/classes/Dancer2-Core/*.t t/classes/Dancer2/*.t t/dsl/*.t t/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-639/fails/*.t t/issues/gh-639/succeeds/*.t t/issues/gh-650/*.t t/issues/gh-975/*.t t/issues/memleak/*.t t/plugin2/*.t t/roles/*.t t/route-pod-coverage/*.t t/scope_problems/*.t t/template_tiny/*.t"
+ }
+);
+
+
+my %FallbackPrereqs = (
+ "App::Cmd::Setup" => 0,
+ "Attribute::Handlers" => 0,
+ "Capture::Tiny" => "0.12",
+ "Carp" => 0,
+ "Clone" => 0,
+ "Config::Any" => 0,
+ "Digest::SHA" => 0,
+ "Encode" => 0,
+ "Exporter" => "5.57",
+ "Exporter::Tiny" => 0,
+ "ExtUtils::MakeMaker" => "7.1101",
+ "File::Basename" => 0,
+ "File::Copy" => 0,
+ "File::Find" => 0,
+ "File::Path" => 0,
+ "File::Share" => 0,
+ "File::Spec" => 0,
+ "File::Temp" => 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::Runtime" => 0,
+ "Moo" => "2.000000",
+ "Moo::Role" => 0,
+ "POSIX" => 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,
+ "Template" => 0,
+ "Template::Tiny" => 0,
+ "Test::Builder" => 0,
+ "Test::CPAN::Meta" => 0,
+ "Test::EOL" => 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.035
+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.035
+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 $WriteMakefileArgs{$mm_key}{$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);
+}
diff --git a/cpanfile b/cpanfile
new file mode 100644
index 00000000..8ed36663
--- /dev/null
+++ b/cpanfile
@@ -0,0 +1,95 @@
+requires 'App::Cmd::Setup';
+requires 'Attribute::Handlers';
+requires 'Carp';
+requires 'Clone';
+requires 'Config::Any';
+requires 'Digest::SHA';
+requires 'Encode';
+requires 'Exporter', '5.57';
+requires 'Exporter::Tiny';
+requires 'File::Basename';
+requires 'File::Copy';
+requires 'File::Find';
+requires 'File::Path';
+requires 'File::Share';
+requires 'File::Spec';
+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 'Plack', '1.0040';
+requires 'Plack::Middleware::FixMissingBodyInRedirect';
+requires 'Plack::Middleware::RemoveRedundantBody';
+requires 'POSIX';
+requires 'Ref::Util';
+requires 'Role::Tiny', '2.000000';
+requires 'Safe::Isa';
+requires 'Sub::Quote';
+requires 'Template';
+requires 'Template::Tiny';
+requires 'Test::Builder';
+requires 'Test::More';
+requires 'Types::Standard';
+requires 'Type::Tiny', '1.000006';
+requires 'URI::Escape';
+
+# Minimum version of YAML is needed due to:
+# - https://github.com/PerlDancer/Dancer2/issues/899
+# Excluded 1.16 is needs due to:
+# - http://www.cpantesters.org/cpan/report/25911c10-4199-11e6-8d7d-86c55bc2a771
+# - http://www.cpantesters.org/cpan/report/284ac158-419a-11e6-9a35-e3e15bc2a771
+requires 'YAML', '0.86';
+conflicts 'YAML', '1.16';
+
+recommends 'CGI::Deurl::XS';
+recommends 'Class::XSAccessor';
+recommends 'Cpanel::JSON::XS';
+recommends 'Crypt::URandom';
+recommends 'HTTP::XSCookies', '0.000007';
+recommends 'HTTP::XSHeaders';
+recommends 'Math::Random::ISAAC::XS';
+recommends 'MooX::TypeTiny';
+recommends 'Pod::Simple::Search';
+recommends 'Pod::Simple::SimpleTree';
+recommends 'Scope::Upper';
+recommends 'Type::Tiny::XS';
+recommends 'URL::Encode::XS';
+recommends 'YAML::XS';
+
+suggests 'Fcntl';
+suggests 'MIME::Types';
+
+test_requires 'Capture::Tiny', '0.12';
+test_requires 'HTTP::Cookies';
+test_requires 'HTTP::Headers';
+test_requires 'Template';
+test_requires 'Test::Builder';
+test_requires 'Test::EOL';
+test_requires 'Test::Fatal';
+test_requires 'Test::More';
+test_requires 'Test::More', '0.92';
+
+build_requires 'Test::CPAN::Meta';
+
+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';
diff --git a/examples/single/hello_world.psgi b/examples/single/hello_world.psgi
new file mode 100644
index 00000000..65ba6640
--- /dev/null
+++ b/examples/single/hello_world.psgi
@@ -0,0 +1,7 @@
+use Dancer2;
+
+get '/' => sub {
+ return 'Hello World';
+};
+
+__PACKAGE__->to_app;
diff --git a/examples/single/simple_calculator.psgi b/examples/single/simple_calculator.psgi
new file mode 100644
index 00000000..e3d78134
--- /dev/null
+++ b/examples/single/simple_calculator.psgi
@@ -0,0 +1,36 @@
+use Dancer2;
+
+get '/' => sub {
+ return q{Welcome to simple calculator, powered by Dancer2.
+ <a href="/add/2/3">add 2 + 3</a>
+ <a href="/multiply?x=2&y=3">multiply</a>
+ <form method="POST" action="/division">
+ <input name="x"><input name="y">
+ <input type="submit" value="Division">
+ </form>
+};
+};
+
+
+get '/add/:x/:y' => sub {
+ my $x = route_parameters->{'x'};
+ my $y = route_parameters->{'y'};
+
+ return ($x+$y);
+};
+
+get '/multiply' => sub {
+ my $x = query_parameters->{'x'};
+ my $y = query_parameters->{'y'};
+
+ return ($x*$y);
+};
+
+post '/division' => sub {
+ my $x = body_parameters->{'x'};
+ my $y = body_parameters->{'y'};
+
+ return int($x/$y);
+};
+
+__PACKAGE__->to_app;
diff --git a/lib/Dancer2.pm b/lib/Dancer2.pm
new file mode 100644
index 00000000..6ee105df
--- /dev/null
+++ b/lib/Dancer2.pm
@@ -0,0 +1,395 @@
+package Dancer2;
+$Dancer2::VERSION = '0.300000';
+# ABSTRACT: Lightweight yet powerful web application framework
+
+use strict;
+use warnings;
+use List::Util 'first';
+use Module::Runtime 'use_module';
+use Import::Into;
+use Dancer2::Core;
+use Dancer2::Core::App;
+use Dancer2::Core::Runner;
+use Dancer2::FileUtils;
+
+our $AUTHORITY = 'SUKRIA';
+
+sub VERSION { shift->SUPER::VERSION(@_) || '0.000000_000' }
+
+our $runner;
+
+sub runner {$runner}
+sub psgi_app { shift->runner->psgi_app(@_) }
+
+sub import {
+ my ($class, @args) = @_;
+ my ($caller, $script) = caller;
+
+ my @final_args;
+ my $clean_import;
+ foreach my $arg (@args) {
+
+ # ignore, no longer necessary
+ # in the future these will warn as deprecated
+ grep +($arg eq $_), qw<:script :syntax :tests>
+ and next;
+
+ if ($arg eq ':nopragmas') {
+ $clean_import++;
+ next;
+ }
+
+ if (substr($arg, 0, 1) eq '!') {
+ push @final_args, $arg, 1;
+ }
+ else {
+ push @final_args, $arg;
+ }
+ }
+
+ $clean_import
+ or $_->import::into($caller)
+ for qw<strict warnings utf8>;
+
+ scalar @final_args % 2
+ and die q{parameters must be key/value pairs or '!keyword'};
+
+ my %final_args = @final_args;
+
+ my $appname = delete $final_args{appname};
+ $appname ||= $caller;
+
+ # never instantiated the runner, should do it now
+ if (not defined $runner) {
+ $runner = Dancer2::Core::Runner->new();
+ }
+
+ # Search through registered apps, creating a new app object
+ # if we do not find one with the same name.
+ my $app;
+ ($app) = first { $_->name eq $appname } @{$runner->apps};
+
+ if (!$app) {
+
+ # populating with the server's postponed hooks in advance
+ $app = Dancer2::Core::App->new(
+ name => $appname,
+ caller => $script,
+ environment => $runner->environment,
+ postponed_hooks => $runner->postponed_hooks->{$appname} || {},
+ );
+
+ # register the app within the runner instance
+ $runner->register_application($app);
+ }
+
+ _set_import_method_to_caller($caller);
+
+ # use config dsl class, must extend Dancer2::Core::DSL
+ my $config_dsl = $app->setting('dsl_class') || 'Dancer2::Core::DSL';
+ $final_args{dsl} ||= $config_dsl;
+
+ # load the DSL, defaulting to Dancer2::Core::DSL
+ my $dsl = use_module($final_args{dsl})->new(app => $app);
+ $dsl->export_symbols_to($caller, \%final_args);
+}
+
+sub _set_import_method_to_caller {
+ my ($caller) = @_;
+
+ my $import = sub {
+ my ($self, %options) = @_;
+
+ my $with = $options{with};
+ for my $key (keys %$with) {
+ $self->dancer_app->setting($key => $with->{$key});
+ }
+ };
+
+ {
+ ## no critic
+ no strict 'refs';
+ no warnings 'redefine';
+ *{"${caller}::import"} = $import;
+ }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2 - Lightweight yet powerful web application framework
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Dancer2 is the new generation of L<Dancer>, the lightweight web-framework for
+Perl. Dancer2 is a complete rewrite based on L<Moo>.
+
+Dancer2 can optionally use XS modules for speed, but at its core remains
+fatpackable (packable by L<App::FatPacker>) so you could easily deploy Dancer2
+applications on hosts that do not support custom CPAN modules.
+
+Dancer2 is easy and fun:
+
+ use Dancer2;
+ get '/' => sub { "Hello World" };
+ dance;
+
+This is the main module for the Dancer2 distribution. It contains logic for
+creating a new Dancer2 application.
+
+=head2 Documentation Index
+
+Documentation on Dancer2 is split into several sections. 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<Dancer2::Tutorial>.
+
+=item * Dancer2 Manual
+
+L<Dancer2::Manual> 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<DSL Keywords|Dancer2::Manual/DSL KEYWORDS>.
+
+=item * Dancer2 Deployment
+
+For configuration examples of different deployment solutions involving
+Dancer2 and Plack, refer to L<Dancer2::Manual::Deployment>.
+
+=item * Dancer2 Cookbook
+
+Specific examples of code for real-life problems and some 'tricks' for
+applications in Dancer can be found in L<Dancer2::Cookbook>
+
+=item * Dancer2 Config
+
+For configuration file details refer to L<Dancer2::Config>. It is a
+complete list of all configuration options.
+
+=item * Dancer2 Plugins
+
+Refer to L<Dancer2::Plugins> for a partial list of available Dancer2
+plugins. Note that although we try to keep this list up to date we
+expect plugin authors to tell us about new modules.
+
+For information on how to author a plugin, see L<Dancer2::Plugin/Writing the plugin>.
+
+=item * Dancer2 Migration guide
+
+L<Dancer2::Manual::Migration> provides the most up-to-date instruction on
+how to convert a Dancer (1) based application to Dancer2.
+
+=back
+
+=head1 FUNCTIONS
+
+=head2 my $runner=runner();
+
+Returns the current runner. It is of type L<Dancer2::Core::Runner>.
+
+=head1 SECURITY REPORTS
+
+If you need to report a security vulnerability in Dancer2, send all pertinent
+information to L<mailto:dancer-security@dancer.pm>. These matters are taken
+extremely seriously, and will be addressed in the earliest timeframe possible.
+
+=head1 SUPPORT
+
+You are welcome to join our mailing list.
+For subscription information, mail address and archives see
+L<http://lists.preshweb.co.uk/mailman/listinfo/dancer-users>.
+
+We are also on IRC: #dancer on irc.perl.org.
+
+=head1 AUTHORS
+
+=head2 CORE DEVELOPERS
+
+ Alberto Simões
+ Alexis Sukrieh
+ Damien Krotkine
+ David Precious
+ Franck Cuny
+ Jason A. Crome
+ Mickey Nasriachi
+ Peter Mottram (SysPete)
+ Russell Jenkins
+ Sawyer X
+ Stefan Hornburg (Racke)
+ Steven Humphrey
+ Yanick Champoux
+
+=head2 CORE DEVELOPERS EMERITUS
+
+ David Golden
+
+=head2 CONTRIBUTORS
+
+ A. Sinan Unur
+ Abdullah Diab
+ Ahmad M. Zawawi
+ Alex Beamish
+ Alexander Karelas
+ Alexandr Ciornii
+ Andrew Beverley
+ Andrew Grangaard
+ Andrew Inishev
+ Andrew Solomon
+ Andy Jack
+ Ashvini V
+ B10m
+ Bas Bloemsaat
+ baynes
+ Ben Hutton
+ Ben Kaufman
+ biafra
+ Blabos de Blebe
+ Breno G. de Oliveira
+ cdmalon
+ Celogeek
+ Cesare Gargano
+ Charlie Gonzalez
+ chenchen000
+ Chi Trinh
+ Christian Walde
+ Christopher White
+ 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
+ Erik Smit
+ Fayland Lam
+ Gabor Szabo
+ geistteufel
+ Gideon D'souza
+ Gil Magno
+ Glenn Fowler
+ Graham Knop
+ Gregor Herrmann
+ Grzegorz Rożniecki
+ Hobbestigrou
+ Hunter McMillen
+ Ivan Bessarabov
+ Ivan Kruglov
+ JaHIY
+ Jakob Voss
+ James Aitken
+ James Raspass
+ James McCoy
+ Jason Lewis
+ Javier Rojas
+ Jean Stebens
+ Jens Rehsack
+ Joel Berger
+ 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
+ Maurice
+ MaxPerl
+ 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 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 Dondley
+ Tatsuhiko Miyagawa
+ 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) 2019 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
diff --git a/lib/Dancer2/CLI.pm b/lib/Dancer2/CLI.pm
new file mode 100644
index 00000000..bc659de4
--- /dev/null
+++ b/lib/Dancer2/CLI.pm
@@ -0,0 +1,35 @@
+package Dancer2::CLI;
+# ABSTRACT: Dancer2 cli application
+$Dancer2::CLI::VERSION = '0.300000';
+use strict;
+use warnings;
+use App::Cmd::Setup -app;
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::CLI - Dancer2 cli application
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/CLI/Command/gen.pm b/lib/Dancer2/CLI/Command/gen.pm
new file mode 100644
index 00000000..f72170d7
--- /dev/null
+++ b/lib/Dancer2/CLI/Command/gen.pm
@@ -0,0 +1,323 @@
+# ABSTRACT: create new Dancer2 application
+package Dancer2::CLI::Command::gen;
+$Dancer2::CLI::Command::gen::VERSION = '0.300000';
+use strict;
+use warnings;
+
+use App::Cmd::Setup -command;
+
+use HTTP::Tiny;
+use File::Find;
+use File::Path 'mkpath';
+use File::Spec::Functions;
+use File::Share 'dist_dir';
+use File::Basename qw/dirname basename/;
+use Dancer2::Template::Simple;
+use Module::Runtime 'require_module';
+
+my $SKEL_APP_FILE = 'lib/AppFile.pm';
+
+sub description { 'Helper script to create new Dancer2 applications' }
+
+sub opt_spec {
+ return (
+ [ 'application|a=s', 'application name' ],
+ [ 'directory|d=s', 'application folder (default: same as application name)' ],
+ [ 'path|p=s', 'application path (default: current directory)',
+ { default => '.' } ],
+ [ 'overwrite|o', 'overwrite existing files' ],
+ [ 'no-check|x', 'don\'t check latest Dancer2 version (requires internet)' ],
+ [ 'skel|s=s', 'skeleton directory' ],
+ );
+}
+
+sub validate_args {
+ my ($self, $opt, $args) = @_;
+
+ my $name = $opt->{application}
+ or $self->usage_error('Application name must be defined');
+
+ if ( $name =~ /[^\w:]/ || $name =~ /^\d/ || $name =~ /\b:\b|:{3,}/ ) {
+ $self->usage_error(
+ "Invalid application name.\n" .
+ "Application names must not contain single colons, dots, " .
+ "hyphens or start with a number.\n"
+ );
+ }
+
+ my $path = $opt->{path};
+ -d $path or $self->usage_error("directory '$path' does not exist");
+ -w $path or $self->usage_error("directory '$path' is not writeable");
+
+ if ( my $skel_path = $opt->{skel} ) {
+ -d $skel_path
+ or $self->usage_error("skeleton directory '$skel_path' not found");
+ }
+}
+
+sub execute {
+ my ($self, $opt, $args) = @_;
+ $self->_version_check() unless $opt->{'no_check'};
+
+ my $dist_dir = dist_dir('Dancer2');
+ my $skel_dir = $opt->{skel} || catdir($dist_dir, 'skel');
+ -d $skel_dir or die "$skel_dir doesn't exist";
+
+ my $app_name = $opt->{application};
+ my $app_file = _get_app_file($app_name);
+ my $app_path = _get_app_path($opt->{path}, $app_name);
+
+ if( my $dir = $opt->{directory} ) {
+ $app_path = catdir( $opt->{path}, $dir );
+ }
+
+ my $files_to_copy = _build_file_list($skel_dir, $app_path);
+ foreach my $pair (@$files_to_copy) {
+ if ($pair->[0] =~ m/$SKEL_APP_FILE$/) {
+ $pair->[1] = catfile($app_path, $app_file);
+ last;
+ }
+ }
+
+ my $vars = {
+ appname => $app_name,
+ appfile => $app_file,
+ appdir => File::Spec->rel2abs($app_path),
+ perl_interpreter => _get_perl_interpreter(),
+ cleanfiles => _get_dashed_name($app_name),
+ dancer_version => $self->version(),
+ };
+
+ _copy_templates($files_to_copy, $vars, $opt->{overwrite});
+ _create_manifest($files_to_copy, $app_path);
+ _add_to_manifest_skip($app_path);
+
+ if ( ! eval { require_module('YAML'); 1; } ) {
+ print <<'NOYAML';
+*****
+WARNING: YAML.pm is not installed. This is not a full dependency, but is highly
+recommended; in particular, the scaffolded Dancer app being created will not be
+able to read settings from the config file without YAML.pm being installed.
+
+To resolve this, simply install YAML from CPAN, for instance using one of the
+following commands:
+
+ cpan YAML
+ perl -MCPAN -e 'install YAML'
+ curl -L http://cpanmin.us | perl - --sudo YAML
+*****
+NOYAML
+ }
+
+ print <<HOWTORUN;
+
+Your new application is ready! To run it:
+
+ cd $app_path
+ plackup bin/app.psgi
+
+If you need community assistance, the following resources are available:
+- Dancer website: http://perldancer.org
+- Mailing list: http://lists.preshweb.co.uk/mailman/listinfo/dancer-users
+- IRC: irc.perl.org#dancer
+
+Happy Dancing!
+
+HOWTORUN
+
+ return 0;
+}
+
+sub version {
+ require_module('Dancer2');
+ return Dancer2->VERSION;
+}
+
+# skel creation routines
+sub _build_file_list {
+ my ($from, $to) = @_;
+ $from =~ s{/+$}{};
+ my $len = length($from) + 1;
+
+ my @result;
+ my $wanted = sub {
+ return unless -f;
+ my $file = substr($_, $len);
+
+ # ignore .git and git/*
+ my $is_git = $file =~ m{^\.git(/|$)}
+ and return;
+
+ push @result, [ $_, catfile($to, $file) ];
+ };
+
+ find({ wanted => $wanted, no_chdir => 1 }, $from);
+ return \@result;
+}
+
+sub _copy_templates {
+ my ($files, $vars, $overwrite) = @_;
+
+ foreach my $pair (@$files) {
+ my ($from, $to) = @{$pair};
+ if (-f $to && !$overwrite) {
+ print "! $to exists, overwrite? [N/y/a]: ";
+ my $res = <STDIN>; chomp($res);
+ $overwrite = 1 if $res eq 'a';
+ next unless ($res eq 'y') or ($res eq 'a');
+ }
+
+ my $to_dir = dirname($to);
+ if (! -d $to_dir) {
+ print "+ $to_dir\n";
+ mkpath $to_dir or die "could not mkpath $to_dir: $!";
+ }
+
+ my $to_file = basename($to);
+ my $ex = ($to_file =~ s/^\+//);
+ $to = catfile($to_dir, $to_file) if $ex;
+
+ print "+ $to\n";
+ my $content;
+
+ {
+ local $/;
+ open(my $fh, '<:raw', $from) or die "unable to open file `$from' for reading: $!";
+ $content = <$fh>;
+ close $fh;
+ }
+
+ if ($from !~ m/\.(ico|jpg|png|css|eot|map|swp|ttf|svg|woff|woff2|js)$/) {
+ $content = _process_template($content, $vars);
+ }
+
+ open(my $fh, '>:raw', $to) or die "unable to open file `$to' for writing: $!";
+ print $fh $content;
+ close $fh;
+
+ if ($ex) {
+ chmod(0755, $to) or warn "unable to change permissions for $to: $!";
+ }
+ }
+}
+
+sub _create_manifest {
+ my ($files, $dir) = @_;
+
+ my $manifest_name = catfile($dir, 'MANIFEST');
+ open(my $manifest, '>', $manifest_name) or die $!;
+ print $manifest "MANIFEST\n";
+
+ foreach my $file (@{$files}) {
+ my $filename = substr $file->[1], length($dir) + 1;
+ my $basename = basename $filename;
+ my $clean_basename = $basename;
+ $clean_basename =~ s/^\+//;
+ $filename =~ s/\Q$basename\E/$clean_basename/;
+ print {$manifest} "$filename\n";
+ }
+
+ close($manifest);
+}
+
+sub _add_to_manifest_skip {
+ my $dir = shift;
+
+ my $filename = catfile($dir, 'MANIFEST.SKIP');
+ open my $fh, '>>', $filename or die $!;
+ print {$fh} "^$dir-\n";
+ close $fh;
+}
+
+sub _process_template {
+ my ($template, $tokens) = @_;
+ my $engine = Dancer2::Template::Simple->new;
+ $engine->{start_tag} = '[d2%';
+ $engine->{stop_tag} = '%2d]';
+ return $engine->render(\$template, $tokens);
+}
+
+sub _get_app_path {
+ my ($path, $appname) = @_;
+ return catdir($path, _get_dashed_name($appname));
+}
+
+sub _get_app_file {
+ my $appname = shift;
+ $appname =~ s{::}{/}g;
+ return catfile('lib', "$appname.pm");
+}
+
+sub _get_perl_interpreter {
+ return -r '/usr/bin/env' ? '#!/usr/bin/env perl' : "#!$^X";
+}
+
+sub _get_dashed_name {
+ my $name = shift;
+ $name =~ s{::}{-}g;
+ return $name;
+}
+
+# version check routines
+sub _version_check {
+ my $self = shift;
+ my $version = $self->version();
+ return if $version =~ m/_/;
+
+ my $latest_version = 0;
+ my $resp = _send_http_request('http://search.cpan.org/api/module/Dancer2');
+
+ if ($resp) {
+ if ( $resp =~ /"version" (?:\s+)? \: (?:\s+)? "(\d\.\d+)"/x ) {
+ $latest_version = $1;
+ } else {
+ die "Can't understand search.cpan.org's reply.\n";
+ }
+ }
+
+ if ($latest_version > $version) {
+ print qq|
+The latest stable Dancer2 release is $latest_version, you are currently using $version.
+Please check http://search.cpan.org/dist/Dancer2/ for updates.
+
+|;
+ }
+}
+
+sub _send_http_request {
+ my $url = shift;
+
+ my $ua = HTTP::Tiny->new( timeout => 5 );
+
+ my $response = $ua->get($url);
+ return $response->{'success'} ? $response->{'content'} : undef;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::CLI::Command::gen - create new Dancer2 application
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/CLI/Command/version.pm b/lib/Dancer2/CLI/Command/version.pm
new file mode 100644
index 00000000..d6c4a720
--- /dev/null
+++ b/lib/Dancer2/CLI/Command/version.pm
@@ -0,0 +1,48 @@
+package Dancer2::CLI::Command::version;
+# ABSTRACT: display version
+$Dancer2::CLI::Command::version::VERSION = '0.300000';
+use strict;
+use warnings;
+use App::Cmd::Setup -command;
+use Module::Runtime 'require_module';
+
+sub description { 'Display version of Dancer2' }
+
+sub command_names {
+ qw/version --version -v/;
+}
+
+sub execute {
+ require_module('Dancer2');
+ print 'Dancer2 ' . Dancer2->VERSION . "\n";
+ return 0;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::CLI::Command::version - display version
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Config.pod b/lib/Dancer2/Config.pod
new file mode 100644
index 00000000..d93eee3f
--- /dev/null
+++ b/lib/Dancer2/Config.pod
@@ -0,0 +1,684 @@
+package Dancer2::Config;
+# ABSTRACT: Configure Dancer2 to suit your needs
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Config - Configure Dancer2 to suit your needs
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+The Dancer2 configuration (as implemented by
+L<Dancer2::Core::Role::ConfigReader>) handles reading and changing the
+configuration of your Dancer2 apps. This document describes how to
+manipulate Dancer2's configuration settings (through code or by file), and
+to document the various settings that are available in Dancer2.
+
+=head1 MANIPULATING SETTINGS VIA CODE
+
+You can change a setting with the keyword C<set>:
+
+ use Dancer2;
+
+ # changing default settings
+ set port => 8080;
+ set content_type => 'text/plain';
+ set startup_info => 0;
+
+=head1 MANIPULATING SETTINGS VIA CONFIGURATION FILES
+
+There's nothing wrong with using C<set> to configure your application. In
+fact you might have some great reasons for doing so. For greater
+flexibility, ease of deployment, etc., you should also consider extracting
+those settings into a configuration file.
+
+=head2 Configuration file path and file names
+
+Dancer2 will first look for the file F<config.EXT> (where F<EXT> is the
+type of configuration file you are using; e.g. F<ini> or F<json> or
+F<yml>) in the root directory of your application. This is considered
+your global Dancer2 config file. If you do not care to have separate
+settings for production and development environments (B<not> a
+recommended practice!), then this file is all you need.
+
+Next, Dancer2 will look for a file called F<config_local.EXT>. This file
+is typically useful for deployment-specific configuration that should
+not be checked into source control. For instance, database credentials
+could be stored in this file. Any settings in this file are merged into
+the existing configuration such that those with the same name in your
+local configuration file will take precedence over those settings in
+the global file.
+
+Next, Dancer2 will look in the F<environments> directory for a configuration
+file specific to the platform you are deploying to (F<production.EXT> or
+F<development.EXT>, for example). Again, the configuration from the environment
+is merged with the existing configuration with the deployment config taking
+precedence.
+
+Finally, Dancer2 will look in the F<environments> directory for a
+local configuration for the specific platform you are deploying to
+(e.g. F<production_local.EXT> or F<development_local.EXT>)
+The configuration in this file is merged as before.
+
+Much like F<config_local.EXT>, this file would be useful for environment-
+specific configuration that would not be checked into source control.
+For instance, when developing an application that talks to multiple services,
+each developer could have their own URLs to those services stored within
+their F<environments/development_local.yaml> file.
+
+Note, if there is no F<config.EXT>, Dancer2 will not look for a
+F<config_local.EXT>. The same is true for the local environment
+configuration.
+
+=head2 Supported configuration file formats
+
+Dancer2 supports any configuration file format that is supported by
+L<Config::Any>. At the time of this writing, that includes YAML (.yml and
+.yaml), JSON (.jsn and .json), INI (.ini), Apache-style configurations (.cnf
+and .conf), XML (.xml), and Perl-style hashes (.pl and .perl).
+
+Dancer2 iterates over these file extensions in the order provided by
+L<Config::Any> and loads any config files that it finds with later
+configuration information overriding earlier config information. To
+restrict which file extension Dancer2 looks for, you may set the
+C<DANCER_CONFIG_EXT> envinroment variable to a specific extension and
+Dancer2 will only look for config files with that extension.
+
+Make sure you pick the appropriate extension for your configuration file
+name, as Dancer2 guesses the type of format based on the file extension.
+
+=head2 Sample configuration files
+
+Note: Not all possibilities are covered here, only the most common options.
+
+If you prefer YAML, a sample YAML based config file might look like this:
+
+ appname: "Hello"
+ charset: "UTF-8"
+ auto_page: 1
+
+ session: "YAML"
+ serializer: "JSON"
+
+ plugins:
+ DBIC:
+ default:
+ dsn: dbi:SQLite:db/mydata.db
+ schema_class: Hello::Schema
+
+If JSON is more your thing, your file might look more like this:
+
+ {
+ "appname": "Hello",
+ "charset": "UTF-8",
+ "auto_page": "1",
+ "session": "YAML",
+ "serializer": "JSON",
+ "plugins": {
+ "DBIC": {
+ "default": {
+ "dsn": "dbi:SQLite:db/mydata.db",
+ "schema_class": "Hello::Schema"
+ }
+ }
+ }
+ }
+
+If you like Apache configuration files, try something similar to:
+
+ appname = Hello
+ charset = UTF-8
+ auto_page = 1
+ session = YAML
+ serializer = JSON
+ <plugins>
+ <DBIC>
+ <default>
+ dsn = dbi =SQLite =db/mydata.db
+ schema_class = Hello = =Schema
+ </default>
+ </DBIC>
+ </plugins>
+
+INI-style files are deliberately simplistic and not recommended for use in
+your Dancer2 applications.
+
+=head1 SUPPORTED SETTINGS
+
+=head2 Run mode and listening interface/port
+
+=head3 server (string)
+
+The IP address that the Dancer2 app should bind to. Default is 0.0.0.0,
+i.e. bind to all available interfaces.
+
+=head3 port (int)
+
+The port Dancer2 will listen to.
+
+Default value is 3000. This setting can be changed on the command-line with
+the B<--port> switch.
+
+=head3 behind_proxy (boolean)
+
+If set to true, Dancer2 will look to C<X-Forwarded-Protocol> and
+C<X-Forwarded-host> when constructing URLs (for example, when using C<redirect>
+or C<host>). This is useful if your application is behind a proxy.
+
+B<Note>: If either of these are missing, the values of the proxy server will be
+used instead. For example, if the client sends a HTTP/1.0 request to a proxy
+that is hosted locally, then C<host> will return the value "localhost". In a
+similar vein, if the client makes a secure connection to the proxy, but the
+proxy does not pass C<X-Forwarded-Protocol>, then C<base> will return
+"http://...". For these reasons, it is recommended that the values are
+hard-configured in the proxy if possible. For Apache this would be:
+
+ RequestHeader set X_FORWARDED_PROTO "https"
+ RequestHeader set X_FORWARDED_HOST "www.example.com"
+
+=head3 no_default_middleware (boolean)
+
+If set to true, your Dancer2 application will B<NOT> be wrapped with the default
+PSGI middleware. The default middleware wrappers are:
+
+=over 4
+
+=item * L<Plack::Middleware::FixMissingBodyInRedirect>
+
+=item * L<Plack::Middleware::Head>
+
+=back
+
+=head2 Content type / character set
+
+=head3 content_type (string)
+
+The default content type of outgoing content. Default value is 'text/html'.
+
+=head3 charset (string)
+
+This setting has multiple effects:
+
+=over
+
+=item *
+
+It sets the default charset of outgoing content. C<charset=> item will be
+added to Content-Type response header.
+
+=item *
+
+It makes Unicode bodies in HTTP responses of C<text/*> types to be encoded
+to this charset.
+
+=item *
+
+It also indicates to Dancer2 in which charset the static files and templates
+are encoded.
+
+=item *
+
+If you're using L<Dancer2::Plugin::Database>, UTF-8 support will
+automatically be enabled for your database - see
+L<Dancer2::Plugin::Database/"AUTOMATIC UTF-8 SUPPORT">
+
+=back
+
+Default value is empty which means don't do anything. HTTP responses without
+charset will be interpreted as ISO-8859-1 by most clients.
+
+You can cancel any charset processing by specifying your own charset in
+Content-Type header or by ensuring that response body leaves your handler
+without Unicode flag set (by encoding it into some 8bit charset, for
+example).
+
+Also, since automatically serialized JSON responses have C<application/json>
+Content-Type, you should always encode them by hand.
+
+=head3 default_mime_type (string)
+
+Dancer2's L<Dancer2::Core::MIME> module uses C<application/data> as a
+default mime type. This setting lets the user change it. For example, if you
+have a lot of files being served in the B<public> folder that do not have an
+extension, and are text files, set the C<default_mime_type> to
+C<text/plain>.
+
+=head2 Serializing responses
+
+=head3 serializer (string)
+
+When writing a webservice, data serialization/deserialization is a common
+issue to deal with. Dancer2 can automatically handle that for you, via a
+serializer.
+
+=head3 Available serializer engines
+
+The following serializers are available, be aware they dynamically depend on
+Perl modules you may not have on your system.
+
+=over 4
+
+=item * B<JSON>
+
+Requires L<JSON>.
+
+=item * B<YAML>
+
+Requires L<YAML>,
+
+=item * B<XML>
+
+Requires L<XML::Simple>.
+
+=item * B<Mutable>
+
+Will try to find the appropriate serializer using the B<Content-Type> and
+B<Accept-type> header of the request.
+
+=back
+
+=head2 Serializer engine
+
+The serializer can be configured in a separate C<engines> section, like so:
+
+ serializer: "JSON"
+
+ engines:
+ serializer:
+ JSON:
+ pretty: 1
+
+See documentation for a particular serializer for supported options.
+
+=head2 File / directory locations
+
+=head3 environment (string)
+
+This is the name of the environment that should be used. Standard Dancer2
+applications have a C<environments> folder with specific configuration files
+for different environments (usually development and production
+environments). They specify different kind of error reporting, deployment
+details, etc. These files are read after the generic C<config.yml>
+configuration file.
+
+=head3 appdir (directory)
+
+This is the path where your application will live. It's where Dancer2 will
+look by default for your config files, templates and static content.
+
+It is typically set by C<use Dancer2> to use the same directory as your
+script.
+
+=head3 public_dir (directory)
+
+This is the directory, where static files are stored. Any existing file in
+that directory will be served as a static file, before matching any route.
+
+See also B<static_handler>.
+
+Default: B<< C<$appdir/public> >>.
+
+=head3 static_handler (boolean)
+
+This setting have to be declared and set to true if you modify standard
+C<public_dir> location.
+
+Default: true if C<$ENV{DANCER_PUBLIC}> is set or C<public_dir> is set to
+B<< C<$appdir/public> >>.
+
+=head3 views (directory)
+
+This is the directory where your templates and layouts live. It's the
+"view" part of MVC (model, view, controller).
+
+Default: B<< C<$appdir/views> >>.
+
+=head2 Templating & layouts
+
+=head3 template
+
+Allows you to configure which template engine should be used. For instance,
+to use Template Toolkit, add the following to C<config.yml>:
+
+ template: template_toolkit
+
+=head3 layout (string)
+
+The name of the layout to use when rendering view. Dancer2 will look for a
+matching template in the directory C<$views/layouts>.
+
+Your can override the default layout using the third argument of the
+C<template> keyword. Check C<Dancer2> manpage for details.
+
+=head3 layout_dir (string)
+
+A relative path where the layouts reside inside the C<views> directory.
+
+ layout_dir: actual_layouts
+
+Default: B<layouts>.
+
+=head2 Logging, debugging and error handling
+
+=head3 startup_info (boolean)
+
+If set to true, prints a banner at the server start with information such as
+versions and the environment (or "dancefloor").
+
+Conforms to the environment variable C<DANCER_STARTUP_INFO>.
+
+=head3 traces (boolean)
+
+If set to true, Dancer2 will display full stack traces when a warning or a
+die occurs. (Internally sets Carp::Verbose). Default to false.
+
+=head3 no_server_tokens (boolean)
+
+If set to true, Dancer2 will B<not> add an "X-Powered-By" header and also append
+the Dancer2 version to the "Server" header. Default to false - adding.
+
+You can also use the environment variable C<DANCER_NO_SERVER_TOKENS>.
+
+=head3 logger (enum)
+
+Select which logger to use. For example, to write to log files with
+L<Dancer2::Logger::File>:
+
+ logger: File
+
+Or to direct log messages to the console from which you started your Dancer2
+app with L<Dancer2::Logger::Console>:
+
+ logger: Console
+
+Loggers are configured with a corresponding L</Logger engine> section, as
+shown below.
+
+=head3 session (enum)
+
+This setting lets you enable a session engine for your web application. By
+default, sessions are disabled in Dancer2, you must choose a session engine
+to use them.
+
+Sessions are configured with a corresponding L</Session engine> section, as
+shown below.
+
+=head3 show_errors (boolean)
+
+If set to true, Dancer2 will render a detailed debug screen whenever an
+error is caught. If set to false, Dancer2 will render the default error
+page, using C<$public/$error_code.html> if it exists, C<$views/$error_code.tt> or the template specified
+by the C<error_template> setting.
+
+The error screen attempts to sanitise sensitive looking information
+(passwords / card numbers in the request, etc) but you still should not have
+show_errors enabled whilst in production, as there is still a risk of
+divulging details.
+
+=head3 error_template (template path)
+
+This setting lets you specify a template to be used in case of runtime
+error. At the present moment the template (as well as C<$views/$error_code.tt> templates) can use four variables:
+
+=over 4
+
+=item B<title>
+
+The error title.
+
+=item B<content>
+
+The error specific content (if any).
+
+=item B<status>
+
+The HTTP status code throwing that error.
+
+=item B<exception>
+
+The stringified exception (e.g. $@) if any.
+
+=back
+
+Keep in mind that 'content' and 'exception' can vary depending on the problem.
+
+For example:
+
+A 404 has an empty 'exception' and 'content' contains the URI that was not found. Unless you do the 404 yourself via C<send_error("You chose ... poorly!", 404);>, then 'content' is 'You chose ... poorly!'.
+
+A 500 because of, say, dividing 0 by 0 will have an empty 'content' and 'exception like 'Illegal division by zero at ...'.
+
+A 401 from C<send_error("You can not know the secret until you sign in grasshopper!", 401);> will have an empty 'exception' and 'content' will contain 'You can not know the secret until you sign in grasshopper!'.
+
+=head2 Logger engine
+
+The logger must be configured in a separate C<engines> section, like so:
+
+ logger: Console
+
+ engines:
+ logger:
+ Console:
+ log_level: core
+
+All loggers support the configuration options below. See documentation for
+a particular logger for other supported options.
+
+=head3 log_level
+
+This option tells which log messages should be actually logged. Possible
+values are B<core>, B<info>, B<debug>, B<warning> or B<error>.
+
+=over 4
+
+=item B<core> : all messages are logged, including some from Dancer2 itself
+
+=item B<debug> : all messages are logged
+
+=item B<info> : only info, warning and error messages are logged
+
+=item B<warning> : only warning and error messages are logged
+
+=item B<error> : only error messages are logged
+
+=back
+
+During development, you'll probably want to use C<debug> to see your own
+debug messages, and C<core> if you need to see what Dancer2 is doing. In
+production, you'll likely want C<error> or C<warning> only, for less-chatty
+logs.
+
+=head2 Session engine
+
+The session engine is configured in the C<engines> section.
+
+ session: Simple
+
+ engines:
+ session:
+ Simple:
+ cookie_name: dance.set
+ cookie_duration: '24 hours'
+ is_secure: 1
+ is_http_only: 1
+
+See L<Dancer2::Core::Role::SessionFactory> for more detailed documentation
+for these options, or the particular session engine for other supported
+options.
+
+=head3 cookie_name
+
+The name of the cookie to store the session ID in. Defaults to
+C<dancer.session>. This can be overridden by certain session engines.
+
+=head3 cookie_domain
+
+The domain of the cookie. By default there is no domain defined for the
+cookie.
+
+=head3 cookie_path
+
+The path of the cookie. By default there is no path defined for the cookie.
+
+=head3 cookie_duration
+
+The session expiry time in seconds, or as e.g. "2 hours" (see
+L<Dancer2::Core::Cookie/expires>. By default, there is no specific expiry
+time.
+
+=head3 is_secure
+
+The user's session ID is stored in a cookie. If the C<is_secure> setting is
+set to a true value, the cookie will be marked as secure, meaning it should
+only be sent over HTTPS connections.
+
+=head3 is_http_only
+
+This setting defaults to 1 and instructs the session cookie to be created
+with the C<HttpOnly> option active, meaning that JavaScript will not be able
+to access to its value.
+
+=head2 auto_page (boolean)
+
+For simple pages where you're not doing anything dynamic, but still want to
+use the template engine to provide headers etc, you can use the auto_page
+feature to avoid the need to create a route for each page.
+
+With C<auto_page> enabled, if the requested path does not match any specific
+route, Dancer2 will check in the views directory for a matching template,
+and use it to satisfy the request if found.
+
+Simply enable auto_page in your config:
+
+ auto_page: 1
+
+Then, if you request C</foo/bar>, Dancer2 will look in the views dir for
+C</foo/bar.tt>.
+
+Dancer2 will honor your C<before_template_render> code, and all default
+variables. They will be accessible and interpolated on automatic served
+pages.
+
+=head2 dsl_class
+
+For complex applications that require extended DSL keywords or other
+functionality the DSL class used can be specified at import time or in the
+config settings.
+
+ dsl_class: 'My::DSL'
+
+This is the same as specifying
+
+ use Dancer2 dsl => 'My::DSL'
+
+in your module. dsl_class defaults to L<Dancer2::Core::DSL> if not specified
+
+=head2 Environment variables
+
+=head3 DANCER_CONFDIR
+
+Sets the configuration directory.
+
+This correlates to the C<confdir> config option.
+
+=head3 DANCER_ENVDIR
+
+Sets the environment directory.
+
+This correlates to the C<envdir> config option.
+
+=head3 PLACK_ENV
+
+Sets the given environment. This can be overridden by
+C<DANCER_ENVIRONMENT>.
+
+=head3 DANCER_ENVIRONMENT
+
+Sets the given environment. This takes higher precedence over
+C<PLACK_ENV>.
+
+If neither C<PLACK_ENV> or C<DANCER_ENVIRONMENT> is set, the environment
+defaults to B<development>.
+
+=head3 DANCER_APPHANDLER
+
+The C<DANCER_APPHANDLER> configuration controls what the C<dance> keyword
+does.
+
+If is set to C<PSGI> (which will automatically be set if C<PLACK_ENV> is
+set), C<dance> will return the PSGI application coderef.
+
+Otherwise (which is the default is - C<Standalone>), it runs the Plack
+standalone server with the application.
+
+=head3 DANCER_PORT
+
+Sets the port which will be used by the development server (if not run
+by L<plackup>).
+
+=head3 DANCER_SERVER
+
+Sets the host the development server will be used by the development
+server (if not run by L<plackup>).
+
+B<Note>: this might change in the future.
+
+=head3 DANCER_STARTUP_INFO
+
+Controls whether to display start up info.
+
+=head3 DANCER_NO_SERVER_TOKENS
+
+Controls whether to display the server tokens.
+
+=head3 DANCER_PUBLIC
+
+Sets the public directory location.
+
+=head3 DANCER_TRACES
+
+Sets the tracing flag which sets L<Carp>'s C<$Verbose> flag.
+
+=head3 DANCER_VIEWS
+
+Sets the views (templates) directory.
+
+=head3 DANCER_LOGGER
+
+Sets the logger engine.
+
+=head3 DANCER_CHARSET
+
+Sets the default charset.
+
+=head3 DANCER_CONTENT_TYPE
+
+Sets the default content type.
+
+If not set, defaults to B<text/html>.
+
+=head1 SEE ALSO
+
+L<Dancer2>
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Cookbook.pod b/lib/Dancer2/Cookbook.pod
new file mode 100644
index 00000000..8fe2144b
--- /dev/null
+++ b/lib/Dancer2/Cookbook.pod
@@ -0,0 +1,995 @@
+package Dancer2::Cookbook;
+# ABSTRACT: Example-driven quick-start to the Dancer2 web framework
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Cookbook - Example-driven quick-start to the Dancer2 web framework
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+A quick-start guide with examples to get you up and running with the Dancer2
+web framework. This document will be twice as useful if you finish reading
+the manual (L<Dancer2::Manual>) first, but that is not required... :-)
+
+=head1 BEGINNER'S DANCE
+
+=head2 A simple Dancer2 web app
+
+Dancer2 has been designed to be easy to work with - it's trivial to write a
+simple web app, but still has the power to work with larger projects. To
+start with, let's make an incredibly simple "Hello World" example:
+
+ #!/usr/bin/env perl
+
+ use Dancer2;
+
+ get '/hello/:name' => sub {
+ return "Why, hello there " . route_parameters->get('name');
+ };
+
+ dance;
+
+Yes - the above is a fully-functioning web app; running that script will
+launch a webserver listening on the default port (3000). Now you can make a
+request:
+
+ $ curl http://localhost:3000/hello/Bob
+ Why, hello there Bob
+
+and it will say hello. The C<:name> part is a named parameter within the
+route specification, whose value is made available through C<route_parameters>.
+
+Note that you don't need to use the C<strict> and C<warnings> pragmas; they
+are already loaded by Dancer2.
+
+=head2 Default Route
+
+In case you want to avoid a I<404 error>, or handle multiple routes in the
+same way and you don't feel like configuring all of them, you can set up a
+default route handler.
+
+The default route handler will handle any request that doesn't get served by
+any other route.
+
+All you need to do is set up the following route as the B<last> route:
+
+ any qr{.*} => sub {
+ status 'not_found';
+ template 'special_404', { path => request->path };
+ };
+
+Then you can set up the template like so:
+
+ You tried to reach [% path %], but it is unavailable at the moment.
+
+ Please try again or contact us at <contact@example.com>.
+
+=head2 Using the C<auto_page> feature for automatic route creation
+
+For simple "static" pages you can simply enable the C<auto_page> config
+setting; this means you don't need to declare a route handler for those
+pages; if a request is for C</foo/bar>, Dancer2 will check for a matching
+view (e.g. C</foo/bar.tt>) and render it with the default layout, if
+found. For full details, see the documentation for the L<auto_page
+setting|Dancer2::Config/"auto_page">.
+
+=head2 Simplifying AJAX queries with the Ajax plugin
+
+As an AJAX query is just an HTTP query, it's similar to a GET or POST route.
+You may ask yourself why you may want to use the C<ajax> keyword (from the
+L<Dancer2::Plugin::Ajax> plugin) instead of a simple C<get>.
+
+Let's say you have a path like C</user/:user> in your application. You may
+want to be able to serve this page with a layout and HTML content. But you
+may also want to be able to call this same url from a javascript query using
+AJAX.
+
+So, instead of having the following code:
+
+ get '/user/:user' => sub {
+ if ( request->is_ajax ) {
+ # create xml, set headers to text/xml, blablabla
+ header( 'Content-Type' => 'text/xml' );
+ header( 'Cache-Control' => 'no-store, no-cache, must-revalidate' );
+ to_xml({...})
+ } else {
+ template users => {...}
+ }
+ };
+
+you can have
+
+ ajax '/user/:user' => sub {
+ to_xml( {...}, RootName => undef );
+ }
+
+and
+
+ get '/user/:user' => sub {
+ template users => {...}
+ }
+
+Because it's an AJAX query, you know you need to return XML content, so
+the content type of the response is set for you.
+
+=head3 Example: Feeding graph data through AJAX
+
+Let us assume we are building an application that uses a plotting library
+to generate a graph and expects to get its data, which is in the form
+of word count from an AJAX call.
+
+For the graph, we need the url I</data> to return a JSON representation
+of the word count data. Dancer in fact has a C<encode_json()> function that takes
+care of the JSON encapsulation.
+
+ get '/data' => sub {
+ open my $fh, '<', $count_file;
+
+ my %contestant;
+ while (<$fh>) {
+ chomp;
+ my ( $date, $who, $count ) = split '\s*,\s*';
+
+ my $epoch = DateTime::Format::Flexible->parse_datetime($date)->epoch;
+ my $time = 1000 * $epoch;
+ $contestant{$who}{$time} = $count;
+ }
+
+ my @json; # data structure that is going to be JSONified
+
+ while ( my ( $peep, $data ) = each %contestant ) {
+ push @json, {
+ label => $peep,
+ hoverable => \1, # so that it becomes JavaScript's 'true'
+ data => [ map { [ $_, $data->{$_} ] }
+ sort { $a <=> $b }
+ keys %$data ],
+ };
+ }
+
+ my $beginning = DateTime::Format::Flexible->parse_datetime( "2010-11-01")->epoch;
+ my $end = DateTime::Format::Flexible->parse_datetime( "2010-12-01")->epoch;
+
+ push @json, {
+ label => 'de par',
+ data => [
+ [$beginning * 1000, 0],
+ [ DateTime->now->epoch * 1_000,
+ 50_000
+ * (DateTime->now->epoch - $beginning)
+ / ($end - $beginning)
+ ]
+ ],
+
+ };
+
+ encode_json( \@json );
+ };
+
+For more serious AJAX interaction, there's also L<Dancer2::Plugin::Ajax>
+that adds an I<ajax> route handler to the mix.
+
+Because it's an AJAX query, you know you need to return XML content, so
+the content type of the response is set for you.
+
+=head2 Using the prefix feature to split your application
+
+For better maintainability, you may want to separate some of your application
+components into different packages. Let's say we have a simple web app with an
+admin section and want to maintain this in a different package:
+
+ package myapp;
+ use Dancer2;
+ use myapp::admin;
+
+ prefix undef;
+
+ get '/' => sub {...};
+
+ 1;
+
+ package myapp::admin;
+ use Dancer2 appname => 'myapp';
+
+ prefix '/admin';
+
+ get '/' => sub {...};
+
+ 1;
+
+The following routes will be generated for us:
+
+ - get /
+ - get /admin/
+ - head /
+ - head /admin/
+
+By default, a separate application is created for every package that uses
+Dancer2. The C<appname> tag is used to collect routes and hooks into a
+single Dancer2 application. In the above example, C<appname =E<gt> 'myapp'>
+adds the routes from C<myapp::admin> to the routes of the app C<myapp>.
+
+When using multiple applications please ensure that your path definitions do
+not overlap. For example, if using a default route as described above, once
+a request is matched to the default route then no further routes (or
+applications) would be reached.
+
+=head2 Delivering custom error pages
+
+=head3 At the Core
+
+In Dancer2, creating new errors is done by creating a new L<Dancer2::Core::Error>
+
+ my $oopsie = Dancer2::Core::Error->new(
+ status => 418,
+ message => "This is the Holidays. Tea not acceptable. We want eggnog.",
+ app => $app,
+ )
+
+If not given, the status code defaults to a 500, there is no need for a message if
+we feel taciturn, and while the C<$app> (which is a I<Dancer2::Core::App>
+object holding all the pieces of information related to the current request) is
+needed if we want to take advantage of the templates, we can also do without.
+
+However, to be seen by the end user, we have to populate the L<Dancer2::Core::Response>
+object with the error's data. This is done via:
+
+ $oopsie->throw($response);
+
+Or, if we want to use the response object already present in the C<$app>
+(which is usually the case):
+
+ $oopsie->throw;
+
+This populates the status code of the response, sets its content, and throws a
+I<halt()> in the dispatch process.
+
+=head3 What it will look like
+
+The error object has quite a few ways to generate its content.
+
+First, it can be explicitly given
+
+ my $oopsie = Dancer2::Core::Error->new(
+ content => '<html><body><h1>OMG</h1></body></html>',
+ );
+
+If the C<$context> was given, the error will check if there is a
+template by the name of the status code (so, say you're using Template
+Toolkit, I<418.tt>) and will use it to generate the content, passing it
+the error's C<$message>, C<$status> code and C<$title> (which, if not
+specified, will be the standard http error definition for the status code).
+
+If there is no template, the error will then look for a static page (to
+continue with our example, I<418.html>) in the I<public/> directory.
+
+And finally, if all of that failed, the error object will fall back on
+an internal template.
+
+=head3 Errors in Routes
+
+The simplest way to use errors in routes is:
+
+ get '/xmas/gift/:gift' => sub {
+ die "sorry, we're all out of ponies\n"
+ if route_parameters->get('gift') eq 'pony';
+ };
+
+The die will be intercepted by Dancer, converted into an error (status
+code 500, message set to the dying words) and passed to the response.
+
+In the cases where more control is required, C<send_error()> is the way to go:
+
+ get '/glass/eggnog' => sub {
+ send_error "Sorry, no eggnog here", 418;
+ };
+
+And if total control is needed:
+
+ get '/xmas/wishlist' => sub {
+ Dancer2::Core::Error->new(
+ response => response(),
+ status => 406,
+ message => "nothing but coal for you, I'm afraid",
+ template => 'naughty/index',
+ )->throw unless user_was_nice();
+
+ ...;
+ };
+
+=head2 Template Toolkit's WRAPPER directive in Dancer2
+
+Dancer2 already provides a WRAPPER-like ability, which we call a "layout".
+The reason we don't use Template Toolkit's WRAPPER (which also makes us
+incompatible with it) is because not all template systems support it.
+In fact, most don't.
+
+However, you might want to use it, and be able to define META variables and
+regular L<Template::Toolkit> variables.
+
+These few steps will get you there:
+
+=over 4
+
+=item * Disable the layout in Dancer2
+
+You can do this by simply commenting (or removing) the C<layout>
+configuration in the config file.
+
+=item * Use the Template Toolkit template engine
+
+Change the configuration of the template to Template Toolkit:
+
+ # in config.yml
+ template: "template_toolkit"
+
+=item * Tell the Template Toolkit engine which wrapper to use
+
+ # in config.yml
+ # ...
+ engines:
+ template:
+ template_toolkit:
+ WRAPPER: layouts/main.tt
+
+=back
+
+Done! Everything will work fine out of the box, including variables and META
+variables.
+
+However, disabling the internal layout it will also disable the hooks C<before_layout_render> and C<after_layout_render>.
+
+=head2 Customizing Template Toolkit in Dancer2
+
+Please see L<Dancer2::Template::TemplateToolkit|Dancer2::Template::TemplateToolkit/"ADVANCED CUSTOMIZATION">
+for more details.
+
+=head2 Accessing configuration information from a separate script
+
+You may want to access your webapp's configuration from outside your
+webapp. You could, of course, use the YAML module of your choice and load
+your webapps's C<config.yml>, but chances are that this is not convenient.
+
+Use Dancer2 instead. You can simply use
+the values from C<config.yml> and some additional default values:
+
+ # bin/show_app_config.pl
+ use Dancer2;
+ printf "template: %s\n", config->{'template'}; # simple
+ printf "log: %s\n", config->{'log'}; # undef
+
+Note that C<< config->{log} >> should result in an uninitialized warning
+on a default scaffold since the environment isn't loaded and
+log is defined in the environment and not in C<config.yml>. Hence C<undef>.
+
+Dancer2 will load your C<config.yml> configuration file along with the
+correct environment file located in your C<environments> directory.
+
+The environment is determined by two environment variables in the following
+order:
+
+=over 4
+
+=item * DANCER_ENVIRONMENT
+
+=item * PLACK_ENV
+
+=back
+
+If neither of those is set, it will default to loading the development
+environment (typically C<$webapp/environment/development.yml>).
+
+If you wish to load a different environment, you need to override these
+variables.
+
+You can call your script with the environment changed:
+
+ $ PLACK_ENV=production perl bin/show_app_config.pl
+
+Or you can override them directly in the script (less recommended):
+
+ BEGIN { $ENV{'DANCER_ENVIRONMENT'} = 'production' }
+ use Dancer2;
+
+ ...
+
+=head2 Using DBIx::Class
+
+L<DBIx::Class>, also known as DBIC, is one of the many Perl ORM
+(I<Object Relational Mapper>). It is easy to use DBIC in Dancer2 using the
+L<Dancer2::Plugin::DBIC>.
+
+=head3 An example
+
+This example demonstrates a simple Dancer2 application that allows one to search
+for authors or books. The application is connected to a database, that contains
+authors, and their books. The website will have one single page with a form,
+that allows one to query books or authors, and display the results.
+
+=head4 Creating the application
+
+ $ dancer2 -a bookstore
+
+To use the Template Toolkit as the template engine, we specify it in the
+configuration file:
+
+ # add in bookstore/config.yml
+ template: template_toolkit
+
+=head4 Creating the view
+
+We need a view to display the search form, and below, the results, if any. The
+results will be fed by the route to the view as an arrayref of results. Each
+result is a I<hashref>, with a author key containing the name of the author, and
+a books key containing an I<arrayref> of strings : the books names.
+
+ # example of a list of results
+ [ { author => 'author 1',
+ books => [ 'book 1', 'book 2' ],
+ },
+ { author => 'author 2',
+ books => [ 'book 3', 'book 4' ],
+ }
+ ]
+
+
+ # bookstore/views/search.tt
+ <p>
+ <form action="/search">
+ Search query: <input type="text" name="query" />
+ </form>
+ </p>
+ <br>
+
+An example of the view, displaying the search form, and the results, if any:
+
+ <% IF query.length %>
+ <p>Search query was : <% query %>.</p>
+ <% IF results.size %>
+ Results:
+ <ul>
+ <% FOREACH result IN results %>
+ <li>Author: <% result.author.replace("((?i)$query)", '<b>$1</b>') %>
+ <ul>
+ <% FOREACH book IN result.books %>
+ <li><% book.replace("((?i)$query)", '<b>$1</b>') %>
+ <% END %>
+ </ul>
+ <% END %>
+ <% ELSE %>
+ No result
+ <% END %>
+ <% END %>
+
+=head4 Creating a Route
+
+A simple route, to be added in the I<bookstore.pm> module:
+
+ # add in bookstore/lib/bookstore.pm
+ get '/search' => sub {
+ my $query = query_parameters->get('query');
+ my @results = ();
+
+ if ( length $query ) {
+ @results = _perform_search($query);
+ }
+
+ template search => {
+ query => $query,
+ results => \@results,
+ };
+ };
+
+=head4 Creating a database
+
+We create a SQLite file database:
+
+ $ sqlite3 bookstore.db
+ CREATE TABLE author(
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ firstname text default '' not null,
+ lastname text not null);
+
+ CREATE TABLE book(
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ author INTEGER REFERENCES author (id),
+ title text default '' not null );
+
+Now, to populate the database with some data, we use L<DBIx::Class>:
+
+ # populate_database.pl
+ package My::Bookstore::Schema;
+ use base qw(DBIx::Class::Schema::Loader);
+ package main;
+ my $schema = My::Bookstore::Schema->connect('dbi:SQLite:dbname=bookstore.db');
+ $schema->populate('Author', [
+ [ 'firstname', 'lastname'],
+ [ 'Ian M.', 'Banks' ],
+ [ 'Richard', 'Matheson'],
+ [ 'Frank', 'Herbert' ],
+ ]);
+ my @books_list = (
+ [ 'Consider Phlebas', 'Banks' ],
+ [ 'The Player of Games', 'Banks' ],
+ [ 'Use of Weapons', 'Banks' ],
+ [ 'Dune', 'Herbert' ],
+ [ 'Dune Messiah', 'Herbert' ],
+ [ 'Children of Dune', 'Herbert' ],
+ [ 'The Night Stalker', 'Matheson' ],
+ [ 'The Night Strangler', 'Matheson' ],
+ );
+ # transform author names into ids
+ $_->[1] = $schema->resultset('Author')->find({ lastname => $_->[1] })->id
+ foreach (@books_list);
+ $schema->populate('Book', [
+ [ 'title', 'author' ],
+ @books_list,
+ ]);
+
+Then run it in the directory where I<bookstore.db> sits:
+
+ perl populate_database.db
+
+=head4 Using Dancer2::Plugin::DBIC
+
+There are 2 ways of configuring DBIC to understand how the data is organized
+in your database:
+
+=over 4
+
+=item * Use auto-detection
+
+The configuration file needs to be updated to indicate the use of the
+Dancer2::Plugin::DBIC plugin, define a new DBIC schema called I<bookstore> and
+to indicate that this schema is connected to the SQLite database we created.
+
+ # add in bookstore/config.yml
+ plugins:
+ DBIC:
+ bookstore:
+ dsn: "dbi:SQLite:dbname=bookstore.db"
+
+Now, C<_perform_search> can be implemented using L<Dancer2::Plugin::DBIC>. The
+plugin gives you access to an additional keyword called B<schema>, which you
+give the name of schema you want to retrieve. It returns a C<DBIx::Class::Schema::Loader>
+which can be used to get a resultset and perform searches, as per standard
+usage of DBIX::Class.
+
+ # add in bookstore/lib/bookstore.pm
+ sub _perform_search {
+ my ($query) = @_;
+ my $bookstore_schema = schema 'bookstore';
+ my @results;
+ # search in authors
+ my @authors = $bookstore_schema->resultset('Author')->search({
+ -or => [
+ firstname => { like => "%$query%" },
+ lastname => { like => "%$query%" },
+ ]
+ });
+ push @results, map {
+ { author => join(' ', $_->firstname, $_->lastname),
+ books => [],
+ }
+ } @authors;
+ my %book_results;
+ # search in books
+ my @books = $bookstore_schema->resultset('Book')->search({
+ title => { like => "%$query%" },
+ });
+ foreach my $book (@books) {
+ my $author_name = join(' ', $book->author->firstname, $book->author->lastname);
+ push @{$book_results{$author_name}}, $book->title;
+ }
+ push @results, map {
+ { author => $_,
+ books => $book_results{$_},
+ }
+ } keys %book_results;
+ return @results;
+ }
+
+=item * Use home made schema classes
+
+The L<DBIx::Class::MooseColumns> lets you write the DBIC schema classes
+using L<Moose>. The schema classes should be put in a place that Dancer2
+will find. A good place is in I<bookstore/lib/>.
+
+Once your schema classes are in place, all you need to do is modify I<config.yml>
+to specify that you want to use them, instead of the default auto-detection method:
+
+ # change in bookstore/config.yml
+ plugins:
+ DBIC:
+ bookstore:
+ schema_class: My::Bookstore::Schema
+ dsn: "dbi:SQLite:dbname=bookstore.db"
+
+B<Starting the application>:
+Our bookstore lookup application can now be started using the built-in server:
+
+ # start the web application
+ plackup bin/app.psgi
+
+=back
+
+=head2 Authentication
+
+Writing a form for authentication is simple: we check the user credentials
+on a request and decide whether to continue or redirect them to a form.
+The form allows them to submit their username and password and we save that
+and create a session for them so when they now try the original request,
+we recognize them and allow them in.
+
+=head3 Basic Application
+
+The application is fairly simple. We have a route that needs authentication,
+we have a route for showing the login page, and we have a route for posting
+login information and creating a session.
+
+ package MyApp;
+ use Dancer2;
+
+ get '/' => sub {
+ session('user')
+ or redirect('/login');
+
+ template index => {};
+ };
+
+ get '/login' => sub {
+ template login => {};
+ };
+
+ post '/login' => sub {
+ my $username = query_parameters->get('username');
+ my $password = query_parameters->get('password');
+ my $redir_url = query_parameters->get('redirect_url') || '/login';
+
+ $username eq 'john' && $password eq 'correcthorsebatterystaple'
+ or redirect $redir_url;
+
+ session user => $username;
+ redirect $redir_url;
+ };
+
+=head3 Tiny Authentication Helper
+
+L<Dancer2::Plugin::Auth::Tiny> allows you to abstract away not only the
+part that checks whether the session exists, but to also generate a
+redirect with the right path and return URL.
+
+We simply have to define what routes needs a login using Auth::Tiny's
+C<needs> keyword.
+
+ get '/' => needs login => sub {
+ template index => {};
+ };
+
+It creates a proper return URL using C<uri_for> and the address from which
+the user arrived.
+
+We can thus decorate all of our private routes to require authentication in
+this manner. If a user does not have a session, it will automatically forward
+it to I</login>, in which we would render a form for the user to send a login request.
+
+Auth::Tiny even provides a new parameter, C<return_url>, which can be used to send
+the user back to their original requested path.
+
+=head3 Password Hashing
+
+L<Dancer2::Plugin::Passphrase> provides a simple passwords-as-objects interface with
+sane defaults for hashed passwords which you can use in your web application. It uses
+B<bcrypt> as the default but supports anything the L<Digest> interface does.
+
+Assuming we have the original user-creation form submitting a username and password:
+
+ package MyApp;
+ use Dancer2;
+ use Dancer2::Plugin::Passphrase;
+ post '/register' => sub {
+ my $username = query_parameters->get('username');
+ my $password = passphrase(
+ query_parameters->get('password')
+ )->generate;
+
+ # $password is now a hashed password object
+ save_user_in_db( $username, $password->rfc2307 );
+
+ template registered => { success => 1 };
+ };
+
+We can now add the B<POST> method for verifying that username and password:
+
+ post '/login' => sub {
+ my $username = query_parameters->get('username');
+ my $password = query_parameters->get('password');
+ my $saved_pass = fetch_password_from_db($username);
+
+ if ( passphrase($password)->matches($saved_pass) ) {
+ session user => $username;
+ redirect query_parameters->get('return_url') || '/';
+ }
+
+ # let's render instead of redirect...
+ template login => { error => 'Invalid username or password' };
+ };
+
+=head2 Writing a REST application
+
+With Dancer2, it's easy to write REST applications. Dancer2 provides helpers
+to serialize and deserialize for the following data formats:
+
+=over 4
+
+=item JSON
+
+=item YAML
+
+=item XML
+
+=item Data::Dumper
+
+=back
+
+To activate this feature, you only have to set the C<serializer> setting to
+the format you require, for instance in your config file:
+
+ serializer: JSON
+
+Or directly in your code:
+
+ set serializer => 'JSON';
+
+From now, all hashrefs or arrayrefs returned by a route will be serialized
+to the format you chose, and all data received from B<POST> or B<PUT>
+requests will be automatically deserialized.
+
+ get '/hello/:name' => sub {
+ # this structure will be returned to the client as
+ # {"name":"$name"}
+ return { name => query_parameters->get('name') };
+ };
+
+It's possible to let the client choose which serializer to use. For
+this, use the C<mutable> serializer, and an appropriate serializer will be
+chosen from the C<Content-Type> header.
+
+It's also possible to return a custom error using the
+L<send_error|Dancer2::Manual/send_error> keyword. When you don't use a serializer,
+the C<send_error> function will take a string as first parameter (the
+message), and an optional HTTP code. When using a serializer, the message
+can be a string, an arrayref or a hashref:
+
+ get '/hello/:name' => sub {
+ if (...) {
+ send_error("you can't do that");
+ # or
+ send_error({reason => 'access denied', message => "no"});
+ }
+ };
+
+The content of the error will be serialized using the appropriate
+serializer.
+
+=head2 Using the serializer
+
+Serializers essentially do two things:
+
+=over 4
+
+=item * Deserialize incoming requests
+
+When a user makes a request with serialized input, the serializer
+automatically deserializes it into actual input parameters.
+
+=item * Serialize outgoing responses
+
+When you return a data structure from a route, it will automatically
+serialize it for you before returning it to the user.
+
+=back
+
+=head3 Configuring
+
+In order to configure a serializer, you just need to pick which format
+you want for encoding/decoding (e.g. L<JSON|Dancer2::Serializer::JSON>)
+and set it up using the C<serializer> configuration keyword.
+
+It is recommended to explicitly add it in the actual code instead of the
+configuration file so it doesn't apply automatically to every app that
+reads the configuration file (unless that's what you want):
+
+ package MyApp;
+ use Dancer2;
+ set serializer => 'JSON'; # Dancer2::Serializer::JSON
+
+ ...
+
+=head3 Using
+
+Now that we have a serializer set up, we can just return data structures:
+
+ get '/' => sub {
+ return { resources => \%resources };
+ };
+
+When we return this data structure, it will automatically be serialized
+into JSON. No other code is necessary.
+
+We also now receive requests in JSON:
+
+ post '/:entity/:id' => sub {
+ my $entity = route_parameters->get('entity');
+ my $id = route_parameters->get('id');
+
+ # input which was sent serialized
+ my $user = body_parameters->get('user');
+
+ ...
+ };
+
+We can now make a serialized request:
+
+ $ curl -X POST http://ourdomain/person/16 -d '{"user":"sawyer_x"}'
+
+=head3 App-specific feature
+
+Serializers are engines. They affect a Dancer Application, which means
+that once you've set a serializer, B<all> routes within that package
+will be serialized and deserialized. This is how the feature works.
+
+As suggested above, if you would like to have both, you need to create
+another application which will not be serialized.
+
+A common usage for this is an API providing serialized endpoints (and
+receiving serialized requests) and providing rendered pages.
+
+ # MyApp.pm
+ package MyApp;
+ use Dancer2;
+
+ # another useful feature:
+ set auto_page => 1;
+
+ get '/' => sub { template 'index' => {...} };
+
+ # MyApp/API.pm
+ package MyApp::API;
+ use Dancer2;
+ set serializer => 'JSON'; # or any other serializer
+
+ get '/' => sub { +{ resources => \%resources, ... } };
+
+ # user-specific routes, for example
+ prefix '/users' => sub {
+ get '/view' => sub {...};
+ get '/view/:id' => sub {...};
+ put '/add' => sub {...}; # automatically deserialized params
+ };
+
+ ...
+
+Then those will be mounted together for a single app:
+
+ # handler: app.pl:
+ use MyApp;
+ use MyApp::API;
+ use Plack::Builder;
+
+ builder {
+ mount '/' => MyApp->to_app;
+ mount '/api' => MyApp::API->to_app;
+ };
+
+If you want use redirect from a mounted package to the application's root
+URI, L<Dancer2::Plugin::RootURIFor> makes this possible:
+
+ package OurWiki;
+ use Dancer;
+ use Dancer2::Plugin::RootURIFor;
+
+ get '/:some_path' => sub {
+ redirect root_uri_for('/');
+ }
+
+=head3 An example: Writing API interfaces
+
+This example demonstrates an app that makes a request to a weather
+API and then displays it dynamically in a web page.
+
+Other than L<Dancer2> for defining routes, we will use L<HTTP::Tiny>
+to make the weather API request, L<JSON> to decode it from JSON format,
+and finally L<File::Spec> to provide a fully-qualified path to our
+template engine.
+
+ use JSON;
+ use Dancer2;
+ use HTTP::Tiny;
+ use File::Spec;
+
+=head4 Configuration
+
+We use the L<Template::Toolkit> template system for this app.
+Dancer searches for our templates in our views directory, which defaults
+to I<views> directory in our current directory. Since we want to put our
+template in our current directory, we will configure that. However,
+I<Template::Toolkit> does not want us to provide a relative path without
+configuring it to allow it. This is a security issue. So, we're using
+L<File::Spec> to create a full path to where we are.
+
+We also unset the default layout, so Dancer won't try to wrap our template
+with another one. This is a feature in Dancer to allow you to wrap your
+templates with a layout when your templating system doesn't support it. Since
+we're not using a layout here, we don't need it.
+
+ set template => 'template_toolkit'; # set template engine
+ set layout => undef; # disable layout
+ set views => File::Spec->rel2abs('.'); # full path to views
+
+Now, we define our URL:
+
+ my $url = 'http://api.openweathermap.org/data/2.5/weather?id=5110629&units=imperial';
+
+=head4 Route
+
+We will define a main route which, upon a request, will fetch the information
+from the weather API, decode it, and then display it to the user.
+
+Route definition:
+
+ get '/' => sub {
+ ...
+ };
+
+Editing the stub of route dispatching code, we start by making the request
+and decoding it:
+
+ # fetch data
+ my $res = HTTP::Tiny->new->get($url);
+
+ # decode request
+ my $data = decode_json $res->{'content'};
+
+The data is not just a flat hash. It's a deep structure. In this example, we
+will filter it for only the simple keys in the retrieved data:
+
+ my $metrics = { map +(
+ ref $data->{$_} ? () : ( $_ => $data->{$_} )
+ ), keys %{$data} };
+
+All that is left now is to render it:
+
+ template index => { metrics => $metrics };
+
+=head1 NON-STANDARD STEPS
+
+=head2 Turning off warnings
+
+The C<warnings> pragma is already used when one loads Dancer2. However, if
+you I<really> do not want the C<warnings> pragma (for example, due to an
+undesired warning about use of undef values), add a C<no warnings> pragma to
+the appropriate block in your module or psgi file.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core.pm b/lib/Dancer2/Core.pm
new file mode 100644
index 00000000..06af1a0d
--- /dev/null
+++ b/lib/Dancer2/Core.pm
@@ -0,0 +1,51 @@
+package Dancer2::Core;
+# ABSTRACT: Core libraries for Dancer2 2.0
+$Dancer2::Core::VERSION = '0.300000';
+use strict;
+use warnings;
+
+sub camelize {
+ my ($value) = @_;
+
+ my $camelized = '';
+ for my $word ( split /_/, $value ) {
+ $camelized .= ucfirst($word);
+ }
+ return $camelized;
+}
+
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core - Core libraries for Dancer2 2.0
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 FUNCTIONS
+
+=head2 camelize
+
+Camelize a underscore-separated-string.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/App.pm b/lib/Dancer2/Core/App.pm
new file mode 100644
index 00000000..05a6ad90
--- /dev/null
+++ b/lib/Dancer2/Core/App.pm
@@ -0,0 +1,1902 @@
+# ABSTRACT: encapsulation of Dancer2 packages
+package Dancer2::Core::App;
+$Dancer2::Core::App::VERSION = '0.300000';
+use Moo;
+use Carp qw<croak carp>;
+use Scalar::Util 'blessed';
+use Module::Runtime 'is_module_name';
+use Safe::Isa;
+use Sub::Quote;
+use File::Spec;
+use Module::Runtime 'use_module';
+use List::Util ();
+use Ref::Util qw< is_ref is_globref is_scalarref >;
+
+use Plack::App::File;
+use Plack::Middleware::FixMissingBodyInRedirect;
+use Plack::Middleware::Head;
+use Plack::Middleware::Conditional;
+use Plack::Middleware::ConditionalGET;
+
+use Dancer2::FileUtils 'path';
+use Dancer2::Core;
+use Dancer2::Core::Cookie;
+use Dancer2::Core::Error;
+use Dancer2::Core::Types;
+use Dancer2::Core::Route;
+use Dancer2::Core::Hook;
+use Dancer2::Core::Request;
+use Dancer2::Core::Factory;
+
+use Dancer2::Handler::File;
+
+our $EVAL_SHIM; $EVAL_SHIM ||= sub {
+ my $code = shift;
+ $code->(@_);
+};
+
+
+# we have hooks here
+with qw<
+ Dancer2::Core::Role::Hookable
+ Dancer2::Core::Role::ConfigReader
+>;
+
+sub supported_engines { [ qw<logger serializer session template> ] }
+
+sub with_plugins {
+ my ( $self, @plugins ) = @_;
+ return map $self->_with_plugin($_), @plugins;
+
+}
+
+sub _with_plugin {
+ my( $self, $plugin ) = @_;
+
+ if ( is_ref($plugin) ) {
+ # passing the plugin as an already-created object
+
+ # already loaded?
+ if( my ( $already ) = grep { ref($plugin) eq ref $_; } @{ $self->plugins } ) {
+ die "trying to load two different objects for plugin ". ref $plugin
+ if refaddr($plugin) != refaddr $already ;
+
+ }
+ else {
+ push @{ $self->plugins }, $plugin;
+ }
+
+ return $plugin;
+ }
+
+ # short plugin names get Dancer2::Plugin:: prefix
+ # plugin names starting with a '+' are full package names
+ if ( $plugin !~ s/^\+// ) {
+ $plugin =~ s/^(?!Dancer2::Plugin::)/Dancer2::Plugin::/;
+ }
+
+ # check if it's already there
+ if( my ( $already ) = grep { $plugin eq ref $_ } @{ $self->plugins } ) {
+ return $already;
+ }
+
+ push @{ $self->plugins },
+ $plugin = use_module($plugin)->new( app => $self );
+
+ return $plugin;
+}
+
+sub with_plugin {
+ my( $self, $plugin ) = @_;
+
+ croak "expected a single argument"
+ unless @_ == 2;
+
+ ( $self->with_plugins($plugin) )[0];
+}
+
+has _factory => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::Factory'],
+ lazy => 1,
+ default => sub { Dancer2::Core::Factory->new },
+);
+
+has logger_engine => (
+ is => 'ro',
+ isa => ConsumerOf['Dancer2::Core::Role::Logger'],
+ lazy => 1,
+ builder => '_build_logger_engine',
+ writer => 'set_logger_engine',
+);
+
+has session_engine => (
+ is => 'ro',
+ isa => ConsumerOf['Dancer2::Core::Role::SessionFactory'],
+ lazy => 1,
+ builder => '_build_session_engine',
+ writer => 'set_session_engine',
+);
+
+has template_engine => (
+ is => 'ro',
+ isa => ConsumerOf['Dancer2::Core::Role::Template'],
+ lazy => 1,
+ builder => '_build_template_engine',
+ writer => 'set_template_engine',
+);
+
+has serializer_engine => (
+ is => 'ro',
+ isa => ConsumerOf['Dancer2::Core::Role::Serializer'],
+ lazy => 1,
+ builder => '_build_serializer_engine',
+ writer => 'set_serializer_engine',
+ predicate => 'has_serializer_engine',
+);
+
+has '+local_triggers' => (
+ default => sub {
+ my $self = shift;
+ my $triggers = {
+ # general triggers we want to allow, besides engines
+ views => sub {
+ my $self = shift;
+ my $value = shift;
+ $self->template_engine->views($value);
+ },
+
+ layout => sub {
+ my $self = shift;
+ my $value = shift;
+ $self->template_engine->layout($value);
+ },
+
+ layout_dir => sub {
+ my $self = shift;
+ my $value = shift;
+ $self->template_engine->layout_dir($value);
+ },
+
+ log => sub {
+ my ( $self, $value, $config ) = @_;
+
+ # This will allow to set the log level
+ # using: set log => warning
+ $self->logger_engine->log_level($value);
+ },
+ };
+
+ foreach my $engine ( @{ $self->supported_engines } ) {
+ $triggers->{$engine} = sub {
+ my $self = shift;
+ my $value = shift;
+ my $config = shift;
+
+ is_ref($value) and return $value;
+
+ my $build_method = "_build_${engine}_engine";
+ my $setter_method = "set_${engine}_engine";
+ my $engine_instance = $self->$build_method( $value, $config );
+
+ # set the engine with the new value from the builder
+ $self->$setter_method($engine_instance);
+
+ return $engine_instance;
+ };
+ }
+
+ return $triggers;
+ },
+);
+
+sub _build_logger_engine {
+ my $self = shift;
+ my $value = shift;
+ my $config = shift;
+
+ defined $config or $config = $self->config;
+ defined $value or $value = $config->{logger};
+
+ is_ref($value) and return $value;
+
+ # XXX This is needed for the tests that create an app without
+ # a runner.
+ defined $value or $value = 'console';
+
+ is_module_name($value)
+ or croak "Cannot load logger engine '$value': illegal module name";
+
+ my $engine_options =
+ $self->_get_config_for_engine( logger => $value, $config );
+
+ my $logger = $self->_factory->create(
+ logger => $value,
+ %{$engine_options},
+ location => $self->config_location,
+ environment => $self->environment,
+ app_name => $self->name,
+ postponed_hooks => $self->postponed_hooks
+ );
+
+ exists $config->{log} and $logger->log_level($config->{log});
+
+ return $logger;
+}
+
+sub _build_session_engine {
+ my $self = shift;
+ my $value = shift;
+ my $config = shift;
+
+ defined $config or $config = $self->config;
+ defined $value or $value = $config->{'session'} || 'simple';
+
+ is_ref($value) and return $value;
+
+ is_module_name($value)
+ or croak "Cannot load session engine '$value': illegal module name";
+
+ my $engine_options =
+ $self->_get_config_for_engine( session => $value, $config );
+
+ Scalar::Util::weaken( my $weak_self = $self );
+
+ # Note that engine options will replace the default session_dir (if provided).
+ return $self->_factory->create(
+ session => $value,
+ session_dir => path( $self->config->{appdir}, 'sessions' ),
+ %{$engine_options},
+ postponed_hooks => $self->postponed_hooks,
+
+ log_cb => sub { $weak_self->log(@_) },
+ );
+}
+
+sub _build_template_engine {
+ my $self = shift;
+ my $value = shift;
+ my $config = shift;
+
+ defined $config or $config = $self->config;
+ defined $value or $value = $config->{'template'};
+
+ defined $value or return;
+ is_ref($value) and return $value;
+
+ is_module_name($value)
+ or croak "Cannot load template engine '$value': illegal module name";
+
+ my $engine_options =
+ $self->_get_config_for_engine( template => $value, $config );
+
+ my $engine_attrs = { config => $engine_options };
+ $engine_attrs->{layout} ||= $config->{layout};
+ $engine_attrs->{views} ||= $config->{views}
+ || path( $self->location, 'views' );
+ $engine_attrs->{layout_dir} ||= $config->{layout_dir}
+ || 'layouts';
+
+ Scalar::Util::weaken( my $weak_self = $self );
+
+ return $self->_factory->create(
+ template => $value,
+ %{$engine_attrs},
+ postponed_hooks => $self->postponed_hooks,
+
+ log_cb => sub { $weak_self->log(@_) },
+ );
+}
+
+sub _build_serializer_engine {
+ my $self = shift;
+ my $value = shift;
+ my $config = shift;
+
+ defined $config or $config = $self->config;
+ defined $value or $value = $config->{serializer};
+
+ defined $value or return;
+ is_ref($value) and return $value;
+
+ my $engine_options =
+ $self->_get_config_for_engine( serializer => $value, $config );
+
+ Scalar::Util::weaken( my $weak_self = $self );
+
+ return $self->_factory->create(
+ serializer => $value,
+ config => $engine_options,
+ postponed_hooks => $self->postponed_hooks,
+
+ log_cb => sub { $weak_self->log(@_) },
+ );
+}
+
+sub _get_config_for_engine {
+ my $self = shift;
+ my $engine = shift;
+ my $name = shift;
+ my $config = shift;
+
+ defined $config->{'engines'} && defined $config->{'engines'}{$engine}
+ or return {};
+
+ # try both camelized name and regular name
+ my $engine_config = {};
+ foreach my $engine_name ( $name, Dancer2::Core::camelize($name) ) {
+ if ( defined $config->{'engines'}{$engine}{$engine_name} ) {
+ $engine_config = $config->{'engines'}{$engine}{$engine_name};
+ last;
+ }
+ }
+
+ return $engine_config;
+}
+
+has postponed_hooks => (
+ is => 'ro',
+ isa => HashRef,
+ default => sub { {} },
+);
+
+# TODO I'd be happier with a HashRef, really
+has plugins => (
+ is => 'rw',
+ isa => ArrayRef,
+ default => sub { [] },
+);
+
+has route_handlers => (
+ is => 'rw',
+ isa => ArrayRef,
+ default => sub { [] },
+);
+
+has name => (
+ is => 'ro',
+ isa => Str,
+ default => sub { (caller(1))[0] },
+);
+
+has request => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::Request'],
+ writer => '_set_request',
+ clearer => 'clear_request',
+ predicate => 'has_request',
+);
+
+sub set_request {
+ my ($self, $request, $defined_engines) = @_;
+ # typically this is passed in as an optimization within the
+ # dispatch loop but may be called elsewhere
+ $defined_engines ||= $self->defined_engines;
+ # populate request in app and all engines
+ $self->_set_request($request);
+ Scalar::Util::weaken( my $weak_request = $request );
+ $_->set_request( $weak_request ) for @{$defined_engines};
+}
+
+has response => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::Response'],
+ lazy => 1,
+ writer => 'set_response',
+ clearer => 'clear_response',
+ builder => '_build_response',
+ predicate => 'has_response',
+);
+
+has with_return => (
+ is => 'ro',
+ predicate => 1,
+ writer => 'set_with_return',
+ clearer => 'clear_with_return',
+);
+
+has session => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::Session'],
+ lazy => 1,
+ builder => '_build_session',
+ writer => 'set_session',
+ clearer => 'clear_session',
+ predicate => '_has_session',
+);
+
+around _build_config => sub {
+ my ( $orig, $self ) = @_;
+ my $config = $self->$orig;
+
+ if ( $config && $config->{'engines'} ) {
+ $self->_validate_engine($_) for keys %{ $config->{'engines'} };
+ }
+
+ return $config;
+};
+
+sub _build_response {
+ my $self = shift;
+ return Dancer2::Core::Response->new(
+ server_tokens => !$self->config->{'no_server_tokens'},
+ $self->has_serializer_engine
+ ? ( serializer => $self->serializer_engine )
+ : (),
+ );
+}
+
+sub _build_session {
+ my $self = shift;
+ my $session;
+
+ # Find the session engine
+ my $engine = $self->session_engine;
+
+ # find the session cookie if any
+ if ( !$self->has_destroyed_session ) {
+ my $session_id;
+ my $session_cookie = $self->cookie( $engine->cookie_name );
+ defined $session_cookie and
+ $session_id = $session_cookie->value;
+
+ # if we have a session cookie, try to retrieve the session
+ if ( defined $session_id ) {
+ eval {
+ $EVAL_SHIM->(sub {
+ $session = $engine->retrieve( id => $session_id );
+ });
+ 1;
+ }
+ or do {
+ my $err = $@ || "Zombie Error";
+ if ( $err !~ /Unable to retrieve session/ ) {
+ croak "Failed to retrieve session: $err"
+ } else {
+ # XXX we throw away the error entirely? Why?
+ }
+ };
+ }
+ }
+
+ # create the session if none retrieved
+ return $session ||= $engine->create();
+}
+
+sub has_session {
+ my $self = shift;
+
+ my $engine = $self->session_engine;
+
+ return $self->_has_session
+ || ( $self->cookie( $engine->cookie_name )
+ && !$self->has_destroyed_session );
+}
+
+has destroyed_session => (
+ is => 'ro',
+ isa => InstanceOf ['Dancer2::Core::Session'],
+ predicate => 1,
+ writer => 'set_destroyed_session',
+ clearer => 'clear_destroyed_session',
+);
+
+has 'prep_apps' => (
+ 'is' => 'ro',
+ 'isa' => ArrayRef,
+ 'default' => sub { [] },
+);
+
+sub find_plugin {
+ my ( $self, $name ) = @_;
+ my $plugin = List::Util::first { ref($_) eq $name } @{ $self->plugins };
+ $plugin or return;
+ return $plugin;
+}
+
+sub destroy_session {
+ my $self = shift;
+
+ # Find the session engine
+ my $engine = $self->session_engine;
+
+ # Expire session, set the expired cookie and destroy the session
+ # Setting the cookie ensures client gets an expired cookie unless
+ # a new session is created and supercedes it
+ my $session = $self->session;
+ $session->expires(-86400); # yesterday
+ $engine->destroy( id => $session->id );
+
+ # Invalidate session cookie in request
+ # and clear session in app and engines
+ $self->set_destroyed_session($session);
+ $self->clear_session;
+ $_->clear_session for @{ $self->defined_engines };
+
+ return;
+}
+
+sub setup_session {
+ my $self = shift;
+
+ for my $engine ( @{ $self->defined_engines } ) {
+ $self->has_session ?
+ $engine->set_session( $self->session ) :
+ $engine->clear_session;
+ }
+}
+
+sub change_session_id {
+ my $self = shift;
+
+ my $session = $self->session;
+
+ # Find the session engine
+ my $engine = $self->session_engine;
+
+ if ($engine->can('_change_id')) {
+
+ # session engine can change session ID
+ $engine->change_id( session => $session );
+ }
+ else {
+
+ # Method order is important in here...
+ #
+ # On session build if there is no destroyed session then the session
+ # builder tries to recreate the session using the existing session
+ # cookie. We really don't want to do that in this case so it is
+ # important to create the new session before the
+ # clear_destroyed_session method is called.
+ #
+ # This sucks.
+ #
+ # Sawyer suggested:
+ #
+ # What if you take the session cookie logic out of that attribute into
+ # another attribute and clear that attribute?
+ # That would force the session rebuilt to rebuilt the attribute and
+ # get a different cookie value, no?
+ #
+ # TODO: think about this some more.
+
+ # grab data, destroy session and store data again
+ my %data = %{$session->data};
+
+ # destroy existing session
+ $self->destroy_session;
+
+ # get new session
+ $session = $self->session;
+
+ # write data from old session into new
+ # Some engines add session id to data so skip id.
+ while (my ($key, $value) = each %data ) {
+ $session->write($key => $value) unless $key eq 'id';
+ }
+
+ # clear out destroyed session - no longer relevant
+ $self->clear_destroyed_session;
+ }
+
+ return $session->id;
+}
+
+has prefix => (
+ is => 'rw',
+ isa => Maybe [Dancer2Prefix],
+ predicate => 1,
+ coerce => sub {
+ my $prefix = shift;
+ defined($prefix) and $prefix eq "/" and return;
+ return $prefix;
+ },
+);
+
+# routes registry, stored by method:
+has routes => (
+ is => 'rw',
+ isa => HashRef,
+ default => sub {
+ { get => [],
+ head => [],
+ post => [],
+ put => [],
+ del => [],
+ options => [],
+ };
+ },
+);
+
+# add_hook will add the hook to the first "hook candidate" it finds that support
+# it. If none, then it will try to add the hook to the current application.
+around add_hook => sub {
+ my $orig = shift;
+ my $self = shift;
+
+ # saving caller information
+ my ( $package, $file, $line ) = caller(4); # deep to 4 : user's app code
+ my $add_hook_caller = [ $package, $file, $line ];
+
+ my ($hook) = @_;
+ my $name = $hook->name;
+ my $hook_aliases = $self->all_hook_aliases;
+
+ # look for an alias
+ defined $hook_aliases->{$name} and $name = $hook_aliases->{$name};
+ $hook->name($name);
+
+ # if that hook belongs to the app, register it now and return
+ $self->has_hook($name) and return $self->$orig(@_);
+
+ # at this point the hook name must be formatted like:
+ # '$type.$candidate.$name', eg: 'engine.template.before_render' or
+ # 'plugin.database.before_dbi_connect'
+ my ( $hookable_type, $hookable_name, $hook_name ) = split( /\./, $name );
+
+ ( defined $hookable_name && defined $hook_name )
+ or croak "Invalid hook name `$name'";
+
+ grep /^$hookable_type$/, qw(core engine handler plugin)
+ or croak "Unknown hook type `$hookable_type'";
+
+ # register the hooks for existing hookable candidates
+ foreach my $hookable ( $self->hook_candidates ) {
+ $hookable->has_hook($name) and $hookable->add_hook(@_);
+ }
+
+ # we register the hook for upcoming objects;
+ # that way, each components that can claim the hook will have a chance
+ # to register it.
+
+ my $postponed_hooks = $self->postponed_hooks;
+
+ # Hmm, so the hook was not claimed, at this point we'll cache it and
+ # register it when the owner is instantiated
+ $postponed_hooks->{$hookable_type}{$hookable_name} ||= {};
+ $postponed_hooks->{$hookable_type}{$hookable_name}{$name} ||= {};
+ $postponed_hooks->{$hookable_type}{$hookable_name}{$name}{hook} = $hook;
+ $postponed_hooks->{$hookable_type}{$hookable_name}{$name}{caller} =
+ $add_hook_caller;
+
+};
+
+around execute_hook => sub {
+ my $orig = shift;
+ my $self = shift;
+
+ local $Dancer2::Core::Route::REQUEST = $self->request;
+ local $Dancer2::Core::Route::RESPONSE = $self->response;
+
+ my ( $hook, @args ) = @_;
+ if ( !$self->has_hook($hook) ) {
+ foreach my $cand ( $self->hook_candidates ) {
+ $cand->has_hook($hook) and return $cand->execute_hook(@_);
+ }
+ }
+
+ return $self->$orig(@_);
+};
+
+sub _build_default_config {
+ my $self = shift;
+
+ my $public = $ENV{DANCER_PUBLIC} || path( $self->location, 'public' );
+ return {
+ content_type => ( $ENV{DANCER_CONTENT_TYPE} || 'text/html' ),
+ charset => ( $ENV{DANCER_CHARSET} || '' ),
+ logger => ( $ENV{DANCER_LOGGER} || 'console' ),
+ views => ( $ENV{DANCER_VIEWS}
+ || path( $self->config_location, 'views' ) ),
+ environment => $self->environment,
+ appdir => $self->location,
+ public_dir => $public,
+ template => 'Tiny',
+ route_handlers => [
+ [
+ AutoPage => 1
+ ],
+ ],
+ };
+}
+
+sub _init_hooks {
+ my $self = shift;
+
+ # Hook to flush the session at the end of the request,
+ # this way, we're sure we flush only once per request
+ #
+ # Note: we create a weakened copy $self
+ # before closing over the weakened copy
+ # to avoid circular memory refs.
+ Scalar::Util::weaken(my $app = $self);
+
+ $self->add_hook(
+ Dancer2::Core::Hook->new(
+ name => 'core.app.after_request',
+ code => sub {
+ my $response = $Dancer2::Core::Route::RESPONSE;
+
+ # make sure an engine is defined, if not, nothing to do
+ my $engine = $app->session_engine;
+ defined $engine or return;
+
+ # if a session has been instantiated or we already had a
+ # session, first flush the session so cookie-based sessions can
+ # update the session ID if needed, then set the session cookie
+ # in the response
+ #
+ # if there is NO session object but the request has a cookie with
+ # a session key, create a dummy session with the same ID (without
+ # actually retrieving and flushing immediately) and generate the
+ # cookie header from the dummy session. Lazy Sessions FTW!
+
+ if ( $app->has_session ) {
+ my $session;
+ if ( $app->_has_session ) { # Session object exists
+ $session = $app->session;
+ $session->is_dirty and $engine->flush( session => $session );
+ }
+ else { # Cookie header exists. Create a dummy session object
+ my $cookie = $app->cookie( $engine->cookie_name );
+ my $session_id = $cookie->value;
+ $session = Dancer2::Core::Session->new( id => $session_id );
+ }
+ $engine->set_cookie_header(
+ response => $response,
+ session => $session
+ );
+ }
+ elsif ( $app->has_destroyed_session ) {
+ my $session = $app->destroyed_session;
+ $engine->set_cookie_header(
+ response => $response,
+ session => $session,
+ destroyed => 1
+ );
+ }
+ },
+ )
+ );
+}
+
+sub supported_hooks {
+ qw/
+ core.app.before_request
+ core.app.after_request
+ core.app.route_exception
+ core.app.before_file_render
+ core.app.after_file_render
+ core.error.before
+ core.error.after
+ core.error.init
+ /;
+}
+
+sub hook_aliases {
+ my $self = shift;
+ $self->{'hook_aliases'} ||= {
+ before => 'core.app.before_request',
+ before_request => 'core.app.before_request',
+ after => 'core.app.after_request',
+ after_request => 'core.app.after_request',
+ init_error => 'core.error.init',
+ before_error => 'core.error.before',
+ after_error => 'core.error.after',
+ on_route_exception => 'core.app.route_exception',
+
+ before_file_render => 'core.app.before_file_render',
+ after_file_render => 'core.app.after_file_render',
+ before_handler_file_render => 'handler.file.before_render',
+ after_handler_file_render => 'handler.file.after_render',
+
+
+ # compatibility from Dancer1
+ before_error_render => 'core.error.before',
+ after_error_render => 'core.error.after',
+ before_error_init => 'core.error.init',
+
+ # TODO: call $engine->hook_aliases as needed
+ # But.. currently there are use cases where hook_aliases
+ # are needed before the engines are intiialized :(
+ before_template_render => 'engine.template.before_render',
+ after_template_render => 'engine.template.after_render',
+ before_layout_render => 'engine.template.before_layout_render',
+ after_layout_render => 'engine.template.after_layout_render',
+ before_serializer => 'engine.serializer.before',
+ after_serializer => 'engine.serializer.after',
+ };
+}
+
+sub defined_engines {
+ my $self = shift;
+ return [
+ $self->template_engine,
+ $self->session_engine,
+ $self->logger_engine,
+ $self->has_serializer_engine
+ ? $self->serializer_engine
+ : (),
+ ];
+}
+
+# FIXME not needed anymore, I suppose...
+sub api_version {2}
+
+sub register_plugin {
+ my $self = shift;
+ my $plugin = shift;
+
+ $self->log( core => "Registered $plugin");
+
+ push @{ $self->plugins }, $plugin;
+}
+
+# This method overrides the default one from Role::ConfigReader
+sub settings {
+ my $self = shift;
+ +{ %{ Dancer2::runner()->config }, %{ $self->config } };
+}
+
+sub cleanup {
+ my $self = shift;
+ $self->clear_request;
+ $self->clear_response;
+ $self->clear_session;
+ $self->clear_destroyed_session;
+ # Clear engine attributes
+ for my $engine ( @{ $self->defined_engines } ) {
+ $engine->clear_session;
+ $engine->clear_request;
+ }
+}
+
+sub _validate_engine {
+ my $self = shift;
+ my $name = shift;
+
+ grep +( $_ eq $name ), @{ $self->supported_engines }
+ or croak "Engine '$name' is not supported.";
+}
+
+sub engine {
+ my $self = shift;
+ my $name = shift;
+
+ $self->_validate_engine($name);
+
+ my $attr_name = "${name}_engine";
+ return $self->$attr_name;
+}
+
+sub template {
+ my $self = shift;
+
+ my $template = $self->template_engine;
+ $template->set_settings( $self->config );
+
+ # A session will not exist if there is no request (global keyword)
+ #
+ # A session may exist but the route code may not have instantiated
+ # the session object (sessions are lazy). If this is the case, do
+ # that now, so the templates have the session data for rendering.
+ $self->has_request && $self->has_session && ! $template->has_session
+ and $self->setup_session;
+
+ # return content
+ return $template->process( @_ );
+}
+
+sub hook_candidates {
+ my $self = shift;
+
+ my @engines = @{ $self->defined_engines };
+
+ my @route_handlers;
+ for my $handler ( @{ $self->route_handlers } ) {
+ my $handler_code = $handler->{handler};
+ blessed $handler_code and $handler_code->can('supported_hooks')
+ and push @route_handlers, $handler_code;
+ }
+
+ # TODO : get the list of all plugins registered
+ my @plugins = @{ $self->plugins };
+
+ ( @route_handlers, @engines, @plugins );
+}
+
+sub all_hook_aliases {
+ my $self = shift;
+
+ my $aliases = $self->hook_aliases;
+ for my $plugin ( grep { $_->can('hook_aliases') } @{ $self->plugins } ) {
+ $aliases = { %{$aliases}, %{ $plugin->hook_aliases } };
+ }
+
+ return $aliases;
+}
+
+sub mime_type {
+ my $self = shift;
+ my $runner = Dancer2::runner();
+
+ exists $self->config->{default_mime_type}
+ ? $runner->mime_type->default( $self->config->{default_mime_type} )
+ : $runner->mime_type->reset_default;
+
+ $runner->mime_type;
+}
+
+sub log {
+ my $self = shift;
+ my $level = shift;
+
+ my $logger = $self->logger_engine
+ or croak "No logger defined";
+
+ $logger->$level(@_);
+}
+
+sub send_as {
+ my $self = shift;
+ my ( $type, $data, $options ) = @_;
+ $options ||= {};
+
+ $type or croak "Can not send_as using an undefined type";
+
+ if ( lc($type) eq 'html' || lc($type) eq 'plain' ) {
+ if ( $type ne lc $type ) {
+ local $Carp::CarpLevel = 2;
+ carp sprintf( "Please use %s as the type for 'send_as', not %s", lc($type), $type );
+ }
+
+ $options->{charset} = $self->config->{charset} || 'UTF-8';
+ my $content = Encode::encode( $options->{charset}, $data );
+ $options->{content_type} ||= join '/', 'text', lc $type;
+ $self->send_file( \$content, %$options ); # returns from sub
+ }
+
+ # Try and load the serializer class
+ my $serializer_class = "Dancer2::Serializer::$type";
+ eval {
+ $EVAL_SHIM->(sub {
+ require_module( $serializer_class );
+ });
+ 1;
+ } or do {
+ my $err = $@ || "Zombie Error";
+ croak "Unable to load serializer class for $type: $err";
+ };
+
+ # load any serializer engine config
+ my $engine_options =
+ $self->_get_config_for_engine( serializer => $type, $self->config ) || {};
+ my $serializer = $serializer_class->new( config => $engine_options );
+ my $content = $serializer->serialize( $data );
+ $options->{content_type} ||= $serializer->content_type;
+ $self->send_file( \$content, %$options );
+}
+
+sub send_error {
+ my $self = shift;
+ my ( $message, $status ) = @_;
+
+ my $err = Dancer2::Core::Error->new(
+ message => $message,
+ app => $self,
+ ( status => $status )x!! $status,
+
+ $self->has_serializer_engine
+ ? ( serializer => $self->serializer_engine )
+ : (),
+ )->throw;
+
+ # Immediately return to dispatch if with_return coderef exists
+ $self->has_with_return && $self->with_return->($err);
+ return $err;
+}
+
+sub send_file {
+ my $self = shift;
+ my $thing = shift;
+ my %options = @_;
+
+ my ($content_type, $charset, $file_path);
+
+ # are we're given a filehandle? (based on what Plack::Middleware::Lint accepts)
+ my $is_filehandle = Plack::Util::is_real_fh($thing)
+ || ( is_globref($thing) && *{$thing}{IO} && *{$thing}{IO}->can('getline') )
+ || ( Scalar::Util::blessed($thing) && $thing->can('getline') );
+ my ($fh) = ($thing)x!! $is_filehandle;
+
+ # if we're given an IO::Scalar object, DTRT (take the scalar ref from it)
+ if (Scalar::Util::blessed($thing) && $thing->isa('IO::Scalar')) {
+ $thing = $thing->sref;
+ }
+
+ # if we're given a SCALAR reference, build a filehandle to it
+ if ( is_scalarref($thing) ) {
+ ## no critic qw(InputOutput::RequireCheckedOpen)
+ open $fh, "<", $thing;
+ }
+
+ # If we haven't got a filehandle, create one to the requested content
+ if (! $fh) {
+ my $path = $thing;
+ # remove prefix from given path (if not a filehandle)
+ my $prefix = $self->prefix;
+ if ( $prefix && $prefix ne '/' ) {
+ $path =~ s/^\Q$prefix\E//;
+ }
+ # static file dir - either system root or public_dir
+ my $dir = $options{system_path}
+ ? File::Spec->rootdir
+ : $ENV{DANCER_PUBLIC}
+ || $self->config->{public_dir}
+ || path( $self->location, 'public' );
+
+ $file_path = Dancer2::Handler::File->merge_paths( $path, $dir );
+ my $err_response = sub {
+ my $status = shift;
+ $self->response->status($status);
+ $self->response->header( 'Content-Type', 'text/plain' );
+ $self->response->content( Dancer2::Core::HTTP->status_message($status) );
+ $self->with_return->( $self->response );
+ };
+ $err_response->(403) if !defined $file_path;
+ $err_response->(404) if !-f $file_path;
+ $err_response->(403) if !-r $file_path;
+
+ # Read file content as bytes
+ $fh = Dancer2::FileUtils::open_file( "<", $file_path );
+ binmode $fh;
+ $content_type = Dancer2::runner()->mime_type->for_file($file_path) || 'text/plain';
+ if ( $content_type =~ m!^text/! ) {
+ $charset = $self->config->{charset} || "utf-8";
+ }
+ }
+
+ # Now we are sure we can render the file...
+ $self->execute_hook( 'core.app.before_file_render', $file_path );
+
+ # response content type and charset
+ ( exists $options{'content_type'} ) and $content_type = $options{'content_type'};
+ ( exists $options{'charset'} ) and $charset = $options{'charset'};
+ $content_type .= "; charset=$charset" if $content_type and $charset;
+ ( defined $content_type )
+ and $self->response->header('Content-Type' => $content_type );
+
+ # content disposition
+ ( exists $options{filename} )
+ and $self->response->header( 'Content-Disposition' =>
+ ($options{content_disposition} || "attachment") . "; filename=\"$options{filename}\"" );
+
+ # use a delayed response unless server does not support streaming
+ my $use_streaming = exists $options{streaming} ? $options{streaming} : 1;
+ my $response;
+ my $env = $self->request->env;
+ if ( $env->{'psgi.streaming'} && $use_streaming ) {
+ my $cb = sub {
+ my $responder = $Dancer2::Core::Route::RESPONDER;
+ my $res = $Dancer2::Core::Route::RESPONSE;
+ return $responder->(
+ [ $res->status, $res->headers_to_array, $fh ]
+ );
+ };
+
+ Scalar::Util::weaken( my $weak_self = $self );
+
+ $response = Dancer2::Core::Response::Delayed->new(
+ error_cb => sub { $weak_self->logger_engine->log( warning => @_ ) },
+ cb => $cb,
+ request => $Dancer2::Core::Route::REQUEST,
+ response => $Dancer2::Core::Route::RESPONSE,
+ );
+ }
+ else {
+ $response = $self->response;
+ # direct assignment to hash element, avoids around modifier
+ # trying to serialise this this content.
+ $response->{content} = Dancer2::FileUtils::read_glob_content($fh);
+ $response->is_encoded(1); # bytes are already encoded
+ }
+
+ $self->execute_hook( 'core.app.after_file_render', $response );
+ $self->with_return->( $response );
+}
+
+sub BUILD {
+ my $self = shift;
+ $self->init_route_handlers();
+ $self->_init_hooks();
+}
+
+sub finish {
+ my $self = shift;
+
+ # normalize some values that require calculations
+ defined $self->config->{'static_handler'}
+ or $self->config->{'static_handler'} = -d $self->config->{'public_dir'};
+
+ $self->register_route_handlers;
+ $self->compile_hooks;
+
+ @{$self->plugins}
+ && $self->plugins->[0]->can('_add_postponed_plugin_hooks')
+ && $self->plugins->[0]->_add_postponed_plugin_hooks(
+ $self->postponed_hooks
+ );
+
+ foreach my $prep_cb ( @{ $self->prep_apps } ) {
+ $prep_cb->($self);
+ }
+}
+
+sub init_route_handlers {
+ my $self = shift;
+
+ my $handlers_config = $self->config->{route_handlers};
+ for my $handler_data ( @{$handlers_config} ) {
+ my ($handler_name, $config) = @{$handler_data};
+ $config = {} if !is_ref($config);
+
+ my $handler = $self->_factory->create(
+ Handler => $handler_name,
+ app => $self,
+ %$config,
+ postponed_hooks => $self->postponed_hooks,
+ );
+
+ push @{ $self->route_handlers }, {
+ name => $handler_name,
+ handler => $handler,
+ };
+ }
+}
+
+sub register_route_handlers {
+ my $self = shift;
+ for my $handler ( @{$self->route_handlers} ) {
+ my $handler_code = $handler->{handler};
+ $handler_code->register($self);
+ }
+}
+
+sub compile_hooks {
+ my ($self) = @_;
+
+ for my $position ( $self->supported_hooks ) {
+ my $compiled_hooks = [];
+ for my $hook ( @{ $self->hooks->{$position} } ) {
+ Scalar::Util::weaken( my $app = $self );
+ my $compiled = sub {
+ # don't run the filter if halt has been used
+ $Dancer2::Core::Route::RESPONSE &&
+ $Dancer2::Core::Route::RESPONSE->is_halted
+ and return;
+
+ eval { $EVAL_SHIM->($hook,@_); 1; }
+ or do {
+ my $err = $@ || "Zombie Error";
+ $app->cleanup;
+ $app->log('error', "Exception caught in '$position' filter: $err");
+ croak "Exception caught in '$position' filter: $err";
+ };
+ };
+
+ push @{$compiled_hooks}, $compiled;
+ }
+ $self->replace_hook( $position, $compiled_hooks );
+ }
+}
+
+sub lexical_prefix {
+ my $self = shift;
+ my $prefix = shift;
+ my $cb = shift;
+
+ $prefix eq '/' and undef $prefix;
+
+ # save the app prefix
+ my $app_prefix = $self->prefix;
+
+ # alter the prefix for the callback
+ my $new_prefix =
+ ( defined $app_prefix ? $app_prefix : '' )
+ . ( defined $prefix ? $prefix : '' );
+
+ # if the new prefix is empty, it's a meaningless prefix, just ignore it
+ length $new_prefix and $self->prefix($new_prefix);
+
+ my $err;
+ my $ok= eval { $EVAL_SHIM->($cb); 1 }
+ or do { $err = $@ || "Zombie Error"; };
+
+ # restore app prefix
+ $self->prefix($app_prefix);
+
+ $ok or croak "Unable to run the callback for prefix '$prefix': $err";
+}
+
+sub add_route {
+ my $self = shift;
+ my %route_attrs = @_;
+
+ my $route = Dancer2::Core::Route->new(
+ type_library => $self->config->{type_library},
+ %route_attrs,
+ prefix => $self->prefix,
+ );
+
+ my $method = $route->method;
+
+ push @{ $self->routes->{$method} }, $route;
+
+ return $route;
+}
+
+sub route_exists {
+ my $self = shift;
+ my $route = shift;
+
+ my $routes = $self->routes->{ $route->method };
+
+ foreach my $existing_route (@$routes) {
+ $existing_route->spec_route eq $route->spec_route
+ and return 1;
+ }
+
+ return 0;
+}
+
+sub routes_regexps_for {
+ my $self = shift;
+ my $method = shift;
+
+ return [ map $_->regexp, @{ $self->routes->{$method} } ];
+}
+
+sub cookie {
+ my $self = shift;
+
+ @_ == 1 and return $self->request->cookies->{ $_[0] };
+
+ # writer
+ my ( $name, $value, %options ) = @_;
+ my $c =
+ Dancer2::Core::Cookie->new( name => $name, value => $value, %options );
+ $self->response->push_header( 'Set-Cookie' => $c->to_header );
+}
+
+sub redirect {
+ my $self = shift;
+ my $destination = shift;
+ my $status = shift;
+
+ # RFC 2616 requires an absolute URI with a scheme,
+ # turn the URI into that if it needs it
+
+ # Scheme grammar as defined in RFC 2396
+ # scheme = alpha *( alpha | digit | "+" | "-" | "." )
+ my $scheme_re = qr{ [a-z][a-z0-9\+\-\.]* }ix;
+ if ( $destination !~ m{^ $scheme_re : }x ) {
+ $destination = $self->request->uri_for( $destination, {}, 1 );
+ }
+
+ $self->response->redirect( $destination, $status );
+
+ # Short circuit any remaining before hook / route code
+ # ('pass' and after hooks are still processed)
+ $self->has_with_return
+ and $self->with_return->($self->response);
+}
+
+sub halt {
+ my $self = shift;
+ $self->response->halt( @_ );
+
+ # Short citcuit any remaining hook/route code
+ $self->has_with_return
+ and $self->with_return->($self->response);
+}
+
+sub pass {
+ my $self = shift;
+ $self->response->pass;
+
+ # Short citcuit any remaining hook/route code
+ $self->has_with_return
+ and $self->with_return->($self->response);
+}
+
+sub forward {
+ my $self = shift;
+ my $url = shift;
+ my $params = shift;
+ my $options = shift;
+
+ my $new_request = $self->make_forward_to( $url, $params, $options );
+
+ $self->has_with_return
+ and $self->with_return->($new_request);
+
+ # nothing else will run after this
+}
+
+# Create a new request which is a clone of the current one, apart
+# from the path location, which points instead to the new location
+sub make_forward_to {
+ my $self = shift;
+ my $url = shift;
+ my $params = shift;
+ my $options = shift;
+
+ my $overrides = { PATH_INFO => $url };
+ exists $options->{method} and
+ $overrides->{REQUEST_METHOD} = $options->{method};
+
+ # "clone" the existing request
+ my $new_request = $self->request->_shallow_clone( $params, $overrides );
+
+ # If a session object was created during processing of the original request
+ # i.e. a session object exists but no cookie existed
+ # add a cookie so the dispatcher can assign the session to the appropriate app
+ my $engine = $self->session_engine;
+ $engine && $self->_has_session or return $new_request;
+ my $name = $engine->cookie_name;
+ exists $new_request->cookies->{$name} and return $new_request;
+ $new_request->cookies->{$name} =
+ Dancer2::Core::Cookie->new( name => $name, value => $self->session->id );
+
+ return $new_request;
+}
+
+sub app { shift }
+
+# DISPATCHER
+sub to_app {
+ my $self = shift;
+
+ # build engines
+ {
+ for ( qw<logger session template> ) {
+ my $attr = "${_}_engine";
+ $self->$attr;
+ }
+
+ # the serializer engine does not have a default
+ # and is the only engine that can actually not have a value
+ if ( $self->config->{'serializer'} ) {
+ $self->serializer_engine;
+ }
+ }
+
+ $self->finish;
+
+ my $psgi = sub {
+ my $env = shift;
+
+ # pre-request sanity check
+ my $method = uc $env->{'REQUEST_METHOD'};
+ $Dancer2::Core::Types::supported_http_methods{$method}
+ or return [
+ 405,
+ [ 'Content-Type' => 'text/plain' ],
+ [ "Method Not Allowed\n\n$method is not supported." ]
+ ];
+
+ my $response;
+ eval {
+ $EVAL_SHIM->(sub{ $response = $self->dispatch($env)->to_psgi });
+ 1;
+ } or do {
+ my $err = $@ || "Zombie Error";
+ return [
+ 500,
+ [ 'Content-Type' => 'text/plain' ],
+ [ "Internal Server Error\n\n$err" ],
+ ];
+ };
+
+ return $response;
+ };
+
+ # Only add static content handler if required
+ if ( $self->config->{'static_handler'} ) {
+ # Use App::File to "serve" the static content
+ my $static_app = Plack::App::File->new(
+ root => $self->config->{public_dir},
+ content_type => sub { $self->mime_type->for_name(shift) },
+ )->to_app;
+ # Conditionally use the static handler wrapped with ConditionalGET
+ # when the file exists. Otherwise the request passes into our app.
+ $psgi = Plack::Middleware::Conditional->wrap(
+ $psgi,
+ condition => sub { -f path( $self->config->{public_dir}, shift->{PATH_INFO} ) },
+ builder => sub { Plack::Middleware::ConditionalGET->wrap( $static_app ) },
+ );
+ }
+
+ # Wrap with common middleware
+ if ( ! $self->config->{'no_default_middleware'} ) {
+ # FixMissingBodyInRedirect
+ $psgi = Plack::Middleware::FixMissingBodyInRedirect->wrap( $psgi );
+ # Apply Head. After static so a HEAD request on static content DWIM.
+ $psgi = Plack::Middleware::Head->wrap( $psgi );
+ }
+
+ return $psgi;
+}
+
+sub dispatch {
+ my $self = shift;
+ my $env = shift;
+
+ my $runner = Dancer2::runner();
+ my $request = $runner->{'internal_request'} ||
+ $self->build_request($env);
+ my $cname = $self->session_engine->cookie_name;
+
+ my $defined_engines = $self->defined_engines;
+
+DISPATCH:
+ while (1) {
+ my $http_method = lc $request->method;
+ my $path_info = $request->path_info;
+
+ # Add request to app and engines
+ $self->set_request($request, $defined_engines);
+
+ $self->log( core => "looking for $http_method $path_info" );
+
+ ROUTE:
+ foreach my $route ( @{ $self->routes->{$http_method} } ) {
+ #warn "testing route " . $route->regexp . "\n";
+ # TODO store in route cache
+
+ # go to the next route if no match
+ my $match = $route->match($request)
+ or next ROUTE;
+
+ $request->_set_route_params($match);
+ $request->_set_route_parameters($match);
+ $request->_set_route($route);
+
+ # Add session to app *if* we have a session and the request
+ # has the appropriate cookie header for _this_ app.
+ if ( my $sess = $runner->{'internal_sessions'}{$cname} ) {
+ $self->set_session($sess);
+ }
+
+ # calling the actual route
+ my $response;
+
+ # this is very evil, but allows breaking out of multiple stack
+ # frames without throwing an exception. Avoiding exceptions means
+ # a naive eval won't swallow our flow control mechanisms, and
+ # avoids __DIE__ handlers. It also prevents some cleanup routines
+ # from working, since they are expecting control to return to them
+ # after an eval.
+ DANCER2_CORE_APP_ROUTE_RETURN: {
+ if (!$self->has_with_return) {
+ $self->set_with_return(sub {
+ $response = shift;
+ no warnings 'exiting';
+ last DANCER2_CORE_APP_ROUTE_RETURN;
+ });
+ }
+ $response = $self->_dispatch_route($route);
+ };
+
+ # ensure we clear the with_return handler
+ $self->clear_with_return;
+
+ # handle forward requests
+ if ( ref $response eq 'Dancer2::Core::Request' ) {
+ # this is actually a request, not response
+ # however, we need to clean up the request & response
+ $self->clear_request;
+ $self->clear_response;
+
+ # this is in case we're asked for an old-style dispatching
+ if ( $runner->{'internal_dispatch'} ) {
+ # Get the session object from the app before we clean up
+ # the request context, so we can propagate this to the
+ # next dispatch cycle (if required).
+ $self->_has_session
+ and $runner->{'internal_sessions'}{$cname} =
+ $self->session;
+
+ $runner->{'internal_forward'} = 1;
+ $runner->{'internal_request'} = $response;
+ return $self->response_not_found($request);
+ }
+
+ $request = $response;
+ next DISPATCH;
+ }
+
+ # from here we assume the response is a Dancer2::Core::Response
+
+ # halted response, don't process further
+ if ( $response->is_halted ) {
+ $self->cleanup;
+ delete $runner->{'internal_request'};
+ return $response;
+ }
+
+ # pass the baton if the response says so...
+ if ( $response->has_passed ) {
+ ## A previous route might have used splat, failed
+ ## this needs to be cleaned from the request.
+ exists $request->{_params}{splat}
+ and delete $request->{_params}{splat};
+
+ $response->has_passed(0); # clear for the next round
+
+ # clear the content because if you pass it,
+ # the next route is in charge of catching it
+ $response->clear_content;
+ next ROUTE;
+ }
+
+ # it's just a regular response
+ $self->execute_hook( 'core.app.after_request', $response );
+ $self->cleanup;
+ delete $runner->{'internal_request'};
+
+ return $response;
+ }
+
+ # we don't actually want to continue the loop
+ last;
+ }
+
+ # No response! ensure Core::Dispatcher recognizes this failure
+ # so it can try the next Core::App
+ # and set the created request so we don't create it again
+ # (this is important so we don't ignore the previous body)
+ if ( $runner->{'internal_dispatch'} ) {
+ $runner->{'internal_404'} = 1;
+ $runner->{'internal_request'} = $request;
+ }
+
+ # Render 404 response, cleanup, and return the response.
+ my $response = $self->response_not_found($request);
+ $self->cleanup;
+ return $response;
+}
+
+sub build_request {
+ my ( $self, $env ) = @_;
+
+ # If we have an app, send the serialization engine
+ my $request = Dancer2::Core::Request->new(
+ env => $env,
+ is_behind_proxy => $self->settings->{'behind_proxy'} || 0,
+
+ $self->has_serializer_engine
+ ? ( serializer => $self->serializer_engine )
+ : (),
+ );
+
+ return $request;
+}
+
+# Call any before hooks then the matched route.
+sub _dispatch_route {
+ my ( $self, $route ) = @_;
+
+ local $@;
+ eval {
+ $EVAL_SHIM->(sub {
+ $self->execute_hook( 'core.app.before_request', $self );
+ });
+ 1;
+ } or do {
+ my $err = $@ || "Zombie Error";
+ return $self->response_internal_error($err);
+ };
+ my $response = $self->response;
+
+ if ( $response->is_halted ) {
+ return $self->_prep_response( $response );
+ }
+
+ eval {
+ $EVAL_SHIM->(sub{ $response = $route->execute($self) });
+ 1;
+ } or do {
+ my $err = $@ || "Zombie Error";
+ return $self->response_internal_error($err);
+ };
+
+ return $response;
+}
+
+sub _prep_response {
+ my ( $self, $response, $content ) = @_;
+
+ # The response object has no back references to the content or app
+ # Update the default_content_type of the response if any value set in
+ # config so it can be applied when the response is encoded/returned.
+ my $config = $self->config;
+ if ( exists $config->{content_type}
+ and my $ct = $config->{content_type} ) {
+ $response->default_content_type($ct);
+ }
+
+ # if we were passed any content, set it in the response
+ defined $content && $response->content($content);
+ return $response;
+}
+
+sub response_internal_error {
+ my ( $self, $error ) = @_;
+
+ $self->execute_hook( 'core.app.route_exception', $self, $error );
+ $self->log( error => "Route exception: $error" );
+
+ local $Dancer2::Core::Route::REQUEST = $self->request;
+ local $Dancer2::Core::Route::RESPONSE = $self->response;
+
+ return Dancer2::Core::Error->new(
+ app => $self,
+ status => 500,
+ exception => $error,
+ )->throw;
+}
+
+sub response_not_found {
+ my ( $self, $request ) = @_;
+
+ $self->set_request($request);
+
+ local $Dancer2::Core::Route::REQUEST = $self->request;
+ local $Dancer2::Core::Route::RESPONSE = $self->response;
+
+ my $response = Dancer2::Core::Error->new(
+ app => $self,
+ status => 404,
+ message => $request->path,
+ )->throw;
+
+ $self->cleanup;
+
+ return $response;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::App - encapsulation of Dancer2 packages
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Everything a package that uses Dancer2 does is encapsulated into a
+C<Dancer2::Core::App> instance. This class defines all that can be done in such
+objects.
+
+Mainly, it will contain all the route handlers, the configuration settings and
+the hooks that are defined in the calling package.
+
+Note that with Dancer2, everything that is done within a package is scoped to
+that package, thanks to that encapsulation.
+
+=head1 ATTRIBUTES
+
+=head2 plugins
+
+=head2 runner_config
+
+=head2 default_config
+
+=head2 with_return
+
+Used to cache the coderef that will return from back to the dispatcher, across
+an arbitrary number of stack frames.
+
+=head2 destroyed_session
+
+We cache a destroyed session here; once this is set we must not attempt to
+retrieve the session from the cookie in the request. If no new session is
+created, this is set (with expiration) as a cookie to force the browser to
+expire the cookie.
+
+=head1 METHODS
+
+=head2 has_session
+
+Returns true if session engine has been defined and if either a session
+object has been instantiated or if a session cookie was found and not
+subsequently invalidated.
+
+=head2 change_session_id
+
+Changes the session ID used by the current session. This should be used on
+any change of privilege level, for example on login. Returns the new session
+ID.
+
+=head2 destroy_session
+
+Destroys the current session and ensures any subsequent session is created
+from scratch and not from the request session cookie
+
+=head2 register_plugin
+
+=head2 with_plugins( @plugin_names )
+
+Creates instances of the given plugins and tie them to the app.
+The plugin classes are automatically loaded.
+Returns the newly created plugins.
+
+The plugin names are expected to be without the leading C<Dancer2::Plugin>.
+I.e., use C<Foo> to mean C<Dancer2::Plugin::Foo>.
+
+If a given plugin is already tied to the app, the already-existing
+instance will be used and returned by C<with_plugins> (think of it
+as using a role).
+
+ my @plugins = $app->with_plugins( 'Foo', 'Bar' );
+
+ # now $app uses the plugins Dancer2::Plugin::Foo
+ # and Dancer2::Plugin::Bar
+
+=head2 with_plugin( $plugin_name )
+
+Just like C<with_plugin>, but for a single plugin.
+
+ my $plugin = $app->with_plugin('Foo');
+
+=head2 add_route
+
+Register a new route handler.
+
+ $app->add_route(
+ method => 'get',
+ regexp => '/somewhere',
+ code => sub { ... },
+ options => $conditions,
+ );
+
+Returns a new L<< Dancer2::Core::Route >> object created with the passed
+arguments.
+
+=head2 route_exists
+
+Returns a true value if a route already exists, otherwise false.
+
+ my $route = Dancer2::Core::Route->new(...);
+ if ($app->route_exists($route)) {
+ ...
+ }
+
+=head2 routes_regexps_for
+
+Sugar for getting the ordered list of all registered route regexps by method.
+
+ my $regexps = $app->routes_regexps_for( 'get' );
+
+Returns an ArrayRef with the results.
+
+=head2 redirect($destination, $status)
+
+Sets a redirect in the response object. If $destination is not an absolute URI, then it will
+be made into an absolute URI, relative to the URI in the request.
+
+=head2 halt
+
+Flag the response object as 'halted'.
+
+If called during request dispatch, immediately returns the response
+to the dispatcher and after hooks will not be run.
+
+=head2 pass
+
+Flag the response object as 'passed'.
+
+If called during request dispatch, immediately returns the response
+to the dispatcher.
+
+=head2 forward
+
+Create a new request which is a clone of the current one, apart
+from the path location, which points instead to the new location.
+This is used internally to chain requests using the forward keyword.
+
+This method takes 3 parameters: the url to forward to, followed by an
+optional hashref of parameters added to the current request parameters,
+followed by a hashref of options regarding the redirect, such as
+C<method> to change the request method.
+
+For example:
+
+ forward '/login', { login_failed => 1 }, { method => 'GET' });
+
+=head2 app
+
+Returns itself. This is simply available as a shim to help transition from
+a previous version in which hooks were sent a context object (originally
+C<Dancer2::Core::Context>) which has since been removed.
+
+ # before
+ hook before => sub {
+ my $ctx = shift;
+ my $app = $ctx->app;
+ };
+
+ # after
+ hook before => sub {
+ my $app = shift;
+ };
+
+This meant that C<< $app->app >> would fail, so this method has been provided
+to make it work.
+
+ # now
+ hook before => sub {
+ my $WannaBeCtx = shift;
+ my $app = $WannaBeContext->app; # works
+ };
+
+=head2 lexical_prefix
+
+Allow for setting a lexical prefix
+
+ $app->lexical_prefix('/blog', sub {
+ ...
+ });
+
+All the route defined within the callback will have a prefix appended to the
+current one.
+
+=head2 C< $SIG{__DIE__} > Compatibility via C< $Dancer2::Core::App::EVAL_SHIM >
+
+If an installation wishes to use C< $SIG{__DIE__} > hooks to enhance
+their error handling then it may be required to ensure that certain
+bookkeeping code is executed within every C<eval BLOCK> that Dancer2
+performs. This can be accomplished by overriding the global variable
+C<$Dancer2::Core::App::EVAL_SHIM> with a subroutine which does whatever
+logic is required.
+
+This routine must perform the equivalent of the following subroutine:
+
+ our $EVAL_SHIM = sub {
+ my $code = shift;
+ return $code->(@_);
+ };
+
+An example of overriding this sub might be as follows:
+
+ $Dancer2::Core::App::EVAL_SHIM = sub {
+ my $code = shift;
+ local $IGNORE_EVAL_COUNTER = $IGNORE_EVAL_COUNTER + 1;
+ return $code->(@_);
+ };
+
+B<Note:> that this is a GLOBAL setting, which must be set up before
+any form of dispatch or use of Dancer2.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Cookie.pm b/lib/Dancer2/Core/Cookie.pm
new file mode 100644
index 00000000..f57dabe2
--- /dev/null
+++ b/lib/Dancer2/Core/Cookie.pm
@@ -0,0 +1,250 @@
+package Dancer2::Core::Cookie;
+# ABSTRACT: A cookie representing class
+$Dancer2::Core::Cookie::VERSION = '0.300000';
+use Moo;
+use URI::Escape;
+use Dancer2::Core::Types;
+use Dancer2::Core::Time;
+use Carp 'croak';
+use Ref::Util qw< is_arrayref is_hashref >;
+use overload '""' => \&_get_value;
+
+BEGIN {
+ my $try_xs =
+ exists($ENV{PERL_HTTP_XSCOOKIES}) ? !!$ENV{PERL_HTTP_XSCOOKIES} :
+ exists($ENV{PERL_ONLY}) ? !$ENV{PERL_ONLY} :
+ 1;
+
+ my $use_xs = 0;
+ $try_xs and eval {
+ require HTTP::XSCookies;
+ $use_xs++;
+ };
+ if ( $use_xs ) {
+ *to_header = \&xs_to_header;
+ }
+ else {
+ *to_header = \&pp_to_header;
+ }
+ *_USE_XS = $use_xs ? sub () { !!1 } : sub () { !!0 };
+}
+
+sub xs_to_header {
+ my $self = shift;
+
+ # HTTP::XSCookies can't handle multi-value cookies.
+ return $self->pp_to_header(@_) if @{[ $self->value ]} > 1;
+
+ return HTTP::XSCookies::bake_cookie(
+ $self->name,
+ { value => $self->value,
+ path => $self->path,
+ domain => $self->domain,
+ expires => $self->expires,
+ httponly => !!$self->http_only, # HTTP::XSCookies seems to distinguish between '"0"' and '0'
+ secure => $self->secure,
+ samesite => $self->same_site,
+ }
+ );
+}
+
+sub pp_to_header {
+ my $self = shift;
+
+ my $value = join( '&', map uri_escape($_), $self->value );
+ my $no_httponly = defined( $self->http_only ) && $self->http_only == 0;
+
+ my @headers = $self->name . '=' . $value;
+ push @headers, "Path=" . $self->path if $self->path;
+ push @headers, "Expires=" . $self->expires if $self->expires;
+ push @headers, "Domain=" . $self->domain if $self->domain;
+ push @headers, "SameSite=" . $self->same_site if $self->same_site;
+ push @headers, "Secure" if $self->secure;
+ push @headers, 'HttpOnly' unless $no_httponly;
+
+ return join '; ', @headers;
+}
+
+has value => (
+ is => 'rw',
+ isa => ArrayRef,
+ required => 0,
+ coerce => sub {
+ my $value = shift;
+ my @values =
+ is_arrayref($value) ? @$value
+ : is_hashref($value) ? %$value
+ : ($value);
+ return [@values];
+ },
+);
+
+around value => sub {
+ my $orig = shift;
+ my $self = shift;
+ my $array = $orig->( $self, @_ );
+ return wantarray ? @$array : $array->[0];
+};
+
+# this is only for overloading; need a real sub to refer to, as the Moose
+# attribute accessor won't be available at that point.
+sub _get_value { shift->value }
+
+has name => (
+ is => 'rw',
+ isa => Str,
+ required => 1,
+);
+
+has expires => (
+ is => 'rw',
+ isa => Str,
+ required => 0,
+ coerce => sub {
+ Dancer2::Core::Time->new( expression => $_[0] )->gmt_string;
+ },
+);
+
+has domain => (
+ is => 'rw',
+ isa => Str,
+ required => 0,
+);
+
+has path => (
+ is => 'rw',
+ isa => Str,
+ default => sub {'/'},
+ predicate => 1,
+);
+
+has secure => (
+ is => 'rw',
+ isa => Bool,
+ required => 0,
+ default => sub {0},
+);
+
+has http_only => (
+ is => 'rw',
+ isa => Bool,
+ required => 0,
+ default => sub {1},
+);
+
+has same_site => (
+ is => 'rw',
+ isa => Enum[qw[Strict Lax]],
+ required => 0,
+);
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Cookie - A cookie representing class
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ use Dancer2::Core::Cookie;
+
+ my $cookie = Dancer2::Core::Cookie->new(
+ name => $cookie_name, value => $cookie_value
+ );
+
+ my $value = $cookie->value;
+
+ print "$cookie"; # objects stringify to their value.
+
+=head1 DESCRIPTION
+
+Dancer2::Core::Cookie provides a HTTP cookie object to work with cookies.
+
+=head1 ATTRIBUTES
+
+=head2 value
+
+The cookie's value.
+
+(Note that cookie objects use overloading to stringify to their value, so if
+you say e.g. return "Hi, $cookie", you'll get the cookie's value there.)
+
+In list context, returns a list of potentially multiple values; in scalar
+context, returns just the first value. (So, if you expect a cookie to have
+multiple values, use list context.)
+
+=head2 name
+
+The cookie's name.
+
+=head2 expires
+
+The cookie's expiration date. There are several formats.
+
+Unix epoch time like 1288817656 to mean "Wed, 03-Nov-2010 20:54:16 GMT"
+
+It also supports a human readable offset from the current time such as "2 hours".
+See the documentation of L<Dancer2::Core::Time> for details of all supported
+formats.
+
+=head2 domain
+
+The cookie's domain.
+
+=head2 path
+
+The cookie's path.
+
+=head2 secure
+
+If true, it instructs the client to only serve the cookie over secure
+connections such as https.
+
+=head2 http_only
+
+By default, cookies are created with a property, named C<HttpOnly>,
+that can be used for security, forcing the cookie to be used only by
+the server (via HTTP) and not by any JavaScript code.
+
+If your cookie is meant to be used by some JavaScript code, set this
+attribute to 0.
+
+=head2 same_site
+
+Whether the cookie ought not to be sent along with cross-site requests,
+an enum of either "Strict" or "Lax", default is unset.
+
+=head1 METHODS
+
+=head2 my $cookie=Dancer2::Core::Cookie->new(%opts);
+
+Create a new Dancer2::Core::Cookie object.
+
+You can set any attribute described in the I<ATTRIBUTES> section above.
+
+=head2 my $header=$cookie->to_header();
+
+Creates a proper HTTP cookie header from the content.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/DSL.pm b/lib/Dancer2/Core/DSL.pm
new file mode 100644
index 00000000..8c3a719f
--- /dev/null
+++ b/lib/Dancer2/Core/DSL.pm
@@ -0,0 +1,558 @@
+# ABSTRACT: Dancer2's Domain Specific Language (DSL)
+
+package Dancer2::Core::DSL;
+$Dancer2::Core::DSL::VERSION = '0.300000';
+use Moo;
+use Carp;
+use Module::Runtime 'require_module';
+use Ref::Util qw< is_arrayref >;
+use Dancer2::Core::Hook;
+use Dancer2::FileUtils;
+use Dancer2::Core::Response::Delayed;
+
+with 'Dancer2::Core::Role::DSL';
+
+sub hook_aliases { +{} }
+sub supported_hooks { () }
+
+sub _add_postponed_plugin_hooks {
+ my ( $self, $postponed_hooks) = @_;
+
+ $postponed_hooks = $postponed_hooks->{'plugin'};
+ return unless defined $postponed_hooks;
+
+ for my $plugin ( keys %{$postponed_hooks} ) {
+ for my $name ( keys %{$postponed_hooks->{$plugin} } ) {
+ my $hook = $postponed_hooks->{$plugin}{$name}{hook};
+ my $caller = $postponed_hooks->{$plugin}{$name}{caller};
+
+ $self->has_hook($name)
+ or croak "plugin $plugin does not support the hook `$name'. ("
+ . join( ", ", @{$caller} ) . ")";
+
+ $self->add_hook($hook);
+ }
+ }
+}
+
+sub dsl_keywords {
+
+ # the flag means : 1 = is global, 0 = is not global. global means can be
+ # called from anywhere. not global means must be called from within a route
+ # handler
+ { any => { is_global => 1 },
+ app => { is_global => 1 },
+ captures => { is_global => 0 },
+ config => { is_global => 1 },
+ content => { is_global => 0 },
+ content_type => { is_global => 0 },
+ context => { is_global => 0 },
+ cookie => { is_global => 0 },
+ cookies => { is_global => 0 },
+ dance => { is_global => 1 },
+ dancer_app => { is_global => 1 },
+ dancer_version => { is_global => 1 },
+ dancer_major_version => { is_global => 1 },
+ debug => { is_global => 1 },
+ decode_json => { is_global => 1 },
+ del => { is_global => 1 },
+ delayed => {
+ is_global => 0, prototype => '&@',
+ },
+ dirname => { is_global => 1 },
+ done => { is_global => 0 },
+ dsl => { is_global => 1 },
+ encode_json => { is_global => 1 },
+ engine => { is_global => 1 },
+ error => { is_global => 1 },
+ false => { is_global => 1 },
+ flush => { is_global => 0 },
+ forward => { is_global => 0 },
+ from_dumper => { is_global => 1 },
+ from_json => { is_global => 1 },
+ from_yaml => { is_global => 1 },
+ get => { is_global => 1 },
+ halt => { is_global => 0 },
+ header => { is_global => 0 },
+ headers => { is_global => 0 },
+ hook => { is_global => 1 },
+ info => { is_global => 1 },
+ log => { is_global => 1 },
+ mime => { is_global => 1 },
+ options => { is_global => 1 },
+ param => { is_global => 0 },
+ params => { is_global => 0 },
+ query_parameters => { is_global => 0 },
+ body_parameters => { is_global => 0 },
+ route_parameters => { is_global => 0 },
+ pass => { is_global => 0 },
+ patch => { is_global => 1 },
+ path => { is_global => 1 },
+ post => { is_global => 1 },
+ prefix => { is_global => 1 },
+ prepare_app => {
+ is_global => 1, prototype => '&',
+ },
+ psgi_app => { is_global => 1 },
+ push_header => { is_global => 0 },
+ push_response_header => { is_global => 0 },
+ put => { is_global => 1 },
+ redirect => { is_global => 0 },
+ request => { is_global => 0 },
+ request_header => { is_global => 0 },
+ response => { is_global => 0 },
+ response_header => { is_global => 0 },
+ response_headers => { is_global => 0 },
+ runner => { is_global => 1 },
+ send_as => { is_global => 0 },
+ send_error => { is_global => 0 },
+ send_file => { is_global => 0 },
+ session => { is_global => 0 },
+ set => { is_global => 1 },
+ setting => { is_global => 1 },
+ splat => { is_global => 0 },
+ start => { is_global => 1 },
+ status => { is_global => 0 },
+ template => { is_global => 1 },
+ to_app => { is_global => 1 },
+ to_dumper => { is_global => 1 },
+ to_json => { is_global => 1 },
+ to_yaml => { is_global => 1 },
+ true => { is_global => 1 },
+ upload => { is_global => 0 },
+ uri_for => { is_global => 0 },
+ var => { is_global => 0 },
+ vars => { is_global => 0 },
+ warning => { is_global => 1 },
+ };
+}
+
+sub dancer_app { shift->app }
+sub dancer_version { Dancer2->VERSION }
+
+sub dancer_major_version {
+ return ( split /\./, dancer_version )[0];
+}
+
+sub log { shift->app->log( @_ ) }
+sub debug { shift->app->log( debug => @_ ) }
+sub info { shift->app->log( info => @_ ) }
+sub warning { shift->app->log( warning => @_ ) }
+sub error { shift->app->log( error => @_ ) }
+
+sub true {1}
+sub false {0}
+
+sub dirname { shift and Dancer2::FileUtils::dirname(@_) }
+sub path { shift and Dancer2::FileUtils::path(@_) }
+
+sub config { shift->app->settings }
+
+sub engine { shift->app->engine(@_) }
+
+sub setting { shift->app->setting(@_) }
+
+sub set { shift->setting(@_) }
+
+sub template { shift->app->template(@_) }
+
+sub session {
+ my ( $self, $key, $value ) = @_;
+
+ # shortcut reads if no session exists, so we don't
+ # instantiate sessions for no reason
+ if ( @_ == 2 ) {
+ return unless $self->app->has_session;
+ }
+
+ my $session = $self->app->session
+ || croak "No session available, a session engine needs to be set";
+
+ $self->app->setup_session;
+
+ # return the session object if no key
+ @_ == 1 and return $session;
+
+ # read if a key is provided
+ @_ == 2 and return $session->read($key);
+
+
+ # write to the session or delete if value is undef
+ if ( defined $value ) {
+ $session->write( $key => $value );
+ }
+ else {
+ $session->delete($key);
+ }
+}
+
+sub send_as { shift->app->send_as(@_) }
+
+sub send_error { shift->app->send_error(@_) }
+
+sub send_file { shift->app->send_file(@_) }
+
+#
+# route handlers & friends
+#
+
+sub hook {
+ my ( $self, $name, $code ) = @_;
+ $self->app->add_hook(
+ Dancer2::Core::Hook->new( name => $name, code => $code ) );
+}
+
+sub prefix {
+ my $app = shift->app;
+ @_ == 1
+ ? $app->prefix(@_)
+ : $app->lexical_prefix(@_);
+}
+
+sub halt { shift->app->halt(@_) }
+
+sub del { shift->_normalize_route( [qw/delete /], @_ ) }
+sub get { shift->_normalize_route( [qw/get head/], @_ ) }
+sub options { shift->_normalize_route( [qw/options /], @_ ) }
+sub patch { shift->_normalize_route( [qw/patch /], @_ ) }
+sub post { shift->_normalize_route( [qw/post /], @_ ) }
+sub put { shift->_normalize_route( [qw/put /], @_ ) }
+
+sub prepare_app { push @{ shift->app->prep_apps }, @_ }
+
+sub any {
+ my $self = shift;
+
+ # If they've supplied their own list of methods,
+ # expand del, otherwise give them the default list.
+ if ( is_arrayref($_[0]) ) {
+ s/^del$/delete/ for @{ $_[0] };
+ }
+ else {
+ unshift @_, [qw/delete get head options patch post put/];
+ }
+
+ $self->_normalize_route(@_);
+}
+
+sub _normalize_route {
+ my $app = shift->app;
+ my $methods = shift;
+ my %args;
+
+ # Options are optional, deduce their presence from arg length.
+ # @_ = ( REGEXP, OPTIONS, CODE )
+ # or
+ # @_ = ( REGEXP, CODE )
+ @args{qw/regexp options code/} = @_ == 3 ? @_ : ( $_[0], {}, $_[1] );
+
+ return map $app->add_route( %args, method => $_ ), @{$methods};
+}
+
+#
+# Server startup
+#
+
+# access to the runner singleton
+# will be populated on-the-fly when needed
+# this singleton contains anything needed to start the application server
+sub runner { Dancer2->runner }
+
+# start the server
+sub start { shift->runner->start }
+
+sub dance { shift->start(@_) }
+
+sub psgi_app {
+ my $self = shift;
+
+ $self->app->to_app;
+}
+
+sub to_app { shift->app->to_app }
+
+#
+# Response alterations
+#
+
+sub status {
+ $Dancer2::Core::Route::RESPONSE->status( $_[1] );
+}
+
+sub push_header {
+ # TODO: deprecate old keyword after we have a period of stability
+ # carp "DEPRECATED: please use the 'push_response_header' keyword instead of 'push_header'";
+ goto &push_response_header;
+}
+
+sub push_response_header {
+ shift;
+ $Dancer2::Core::Route::RESPONSE->push_header(@_);
+}
+
+sub header {
+ # TODO: deprecate keyword after a period of stability
+ # carp "DEPRECATED: please use the 'response_header' keyword instead of 'header'";
+ goto &response_header;
+}
+
+sub response_header {
+ shift;
+ $Dancer2::Core::Route::RESPONSE->header(@_);
+}
+
+sub headers {
+ # TODO: deprecate keyword after a period of stability
+ # carp "DEPRECATED: please use the 'response_headers' keyword instead of 'headers'";
+ goto &response_headers;
+}
+
+sub response_headers {
+ shift;
+ $Dancer2::Core::Route::RESPONSE->header(@_);
+}
+
+sub content {
+ my $dsl = shift;
+
+ # simple synchronous response
+ my $responder = $Dancer2::Core::Route::RESPONDER
+ or croak 'Cannot use content keyword outside delayed response';
+
+ # flush if wasn't flushed before
+ if ( !$Dancer2::Core::Route::WRITER ) {
+ $Dancer2::Core::Route::WRITER = $responder->([
+ $Dancer2::Core::Route::RESPONSE->status,
+ $Dancer2::Core::Route::RESPONSE->headers_to_array,
+ ]);
+ }
+
+ eval {
+ $Dancer2::Core::Route::WRITER->write(@_);
+ 1;
+ } or do {
+ my $error = $@ || 'Zombie Error';
+ $Dancer2::Core::Route::ERROR_HANDLER
+ ? $Dancer2::Core::Route::ERROR_HANDLER->($error)
+ : $dsl->app->logger_engine->log(
+ warning => "Error in delayed response: $error"
+ );
+ };
+}
+
+sub content_type {
+ shift;
+ $Dancer2::Core::Route::RESPONSE->content_type(@_);
+}
+
+sub delayed {
+ my ( $dsl, $cb, @args ) = @_;
+
+ @args % 2 == 0
+ or croak 'Arguments to delayed() keyword must be key/value pairs';
+
+ # first time, responder doesn't exist yet
+ my %opts = @args;
+ $Dancer2::Core::Route::RESPONDER
+ or return Dancer2::Core::Response::Delayed->new(
+ cb => $cb,
+ request => $Dancer2::Core::Route::REQUEST,
+ response => $Dancer2::Core::Route::RESPONSE,
+
+ ( error_cb => $opts{'on_error'} )x!! $opts{'on_error'},
+ );
+
+ # we're in an async request process
+ my $request = $Dancer2::Core::Route::REQUEST;
+ my $response = $Dancer2::Core::Route::RESPONSE;
+ my $responder = $Dancer2::Core::Route::RESPONDER;
+ my $writer = $Dancer2::Core::Route::WRITER;
+ my $handler = $Dancer2::Core::Route::ERROR_HANDLER;
+
+ return sub {
+ local $Dancer2::Core::Route::REQUEST = $request;
+ local $Dancer2::Core::Route::RESPONSE = $response;
+ local $Dancer2::Core::Route::RESPONDER = $responder;
+ local $Dancer2::Core::Route::WRITER = $writer;
+ local $Dancer2::Core::Route::ERROR_HANDLER = $handler;
+
+ $cb->(@_);
+ };
+}
+
+sub flush {
+ my $responder = $Dancer2::Core::Route::RESPONDER
+ or croak 'flush() called outside streaming response';
+
+ my $response = $Dancer2::Core::Route::RESPONSE;
+ $Dancer2::Core::Route::WRITER = $responder->([
+ $response->status, $response->headers_to_array,
+ ]);
+}
+
+sub done {
+ my $writer = $Dancer2::Core::Route::WRITER
+ or croak 'done() called outside streaming response';
+
+ $writer->close;
+}
+
+sub pass { shift->app->pass }
+
+#
+# Route handler helpers
+#
+
+sub context {
+ carp "DEPRECATED: please use the 'app' keyword instead of 'context'";
+ shift->app;
+}
+
+sub request { $Dancer2::Core::Route::REQUEST }
+
+sub request_header { shift; $Dancer2::Core::Route::REQUEST->headers->header(@_) }
+
+sub response { $Dancer2::Core::Route::RESPONSE }
+
+sub upload { shift; $Dancer2::Core::Route::REQUEST->upload(@_); }
+
+sub captures { $Dancer2::Core::Route::REQUEST->captures }
+
+sub uri_for { shift; $Dancer2::Core::Route::REQUEST->uri_for(@_); }
+
+sub splat { $Dancer2::Core::Route::REQUEST->splat }
+
+sub params { shift; $Dancer2::Core::Route::REQUEST->params(@_); }
+
+sub param { shift; $Dancer2::Core::Route::REQUEST->param(@_); }
+
+sub query_parameters { shift; $Dancer2::Core::Route::REQUEST->query_parameters(@_); }
+sub body_parameters { shift; $Dancer2::Core::Route::REQUEST->body_parameters(@_); }
+sub route_parameters { shift; $Dancer2::Core::Route::REQUEST->route_parameters(@_); }
+
+sub redirect { shift->app->redirect(@_) }
+
+sub forward { shift->app->forward(@_) }
+
+sub vars { $Dancer2::Core::Route::REQUEST->vars }
+
+sub var { shift; $Dancer2::Core::Route::REQUEST->var(@_); }
+
+sub cookies { $Dancer2::Core::Route::REQUEST->cookies }
+sub cookie { shift->app->cookie(@_) }
+
+sub mime {
+ my $self = shift;
+ if ( $self->app ) {
+ return $self->app->mime_type;
+ }
+ else {
+ my $runner = $self->runner;
+ $runner->mime_type->reset_default;
+ return $runner->mime_type;
+ }
+}
+
+#
+# engines
+#
+
+sub from_json {
+ shift; # remove first element
+ require_module('Dancer2::Serializer::JSON');
+ Dancer2::Serializer::JSON::from_json(@_);
+}
+
+sub to_json {
+ shift; # remove first element
+ require_module('Dancer2::Serializer::JSON');
+ Dancer2::Serializer::JSON::to_json(@_);
+}
+
+sub decode_json {
+ shift; # remove first element
+ require_module('Dancer2::Serializer::JSON');
+ Dancer2::Serializer::JSON::decode_json(@_);
+}
+
+sub encode_json {
+ shift; # remove first element
+ require_module('Dancer2::Serializer::JSON');
+ Dancer2::Serializer::JSON::encode_json(@_);
+}
+
+sub from_yaml {
+ shift; # remove first element
+ require_module('Dancer2::Serializer::YAML');
+ Dancer2::Serializer::YAML::from_yaml(@_);
+}
+
+sub to_yaml {
+ shift; # remove first element
+ require_module('Dancer2::Serializer::YAML');
+ Dancer2::Serializer::YAML::to_yaml(@_);
+}
+
+sub from_dumper {
+ shift; # remove first element
+ require_module('Dancer2::Serializer::Dumper');
+ Dancer2::Serializer::Dumper::from_dumper(@_);
+}
+
+sub to_dumper {
+ shift; # remove first element
+ require_module('Dancer2::Serializer::Dumper');
+ Dancer2::Serializer::Dumper::to_dumper(@_);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::DSL - Dancer2's Domain Specific Language (DSL)
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 FUNCTIONS
+
+=head2 setting
+
+Lets you define settings and access them:
+
+ setting('foo' => 42);
+ setting('foo' => 42, 'bar' => 43);
+ my $foo=setting('foo');
+
+If settings were defined returns number of settings.
+
+=head2 set ()
+
+alias for L<setting>:
+
+ set('foo' => '42');
+ my $port=set('port');
+
+=head1 SEE ALSO
+
+L<http://advent.perldancer.org/2010/18>
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Dispatcher.pm b/lib/Dancer2/Core/Dispatcher.pm
new file mode 100644
index 00000000..b4db7023
--- /dev/null
+++ b/lib/Dancer2/Core/Dispatcher.pm
@@ -0,0 +1,141 @@
+package Dancer2::Core::Dispatcher;
+# ABSTRACT: Class for dispatching request to the appropriate route handler
+$Dancer2::Core::Dispatcher::VERSION = '0.300000';
+use Moo;
+
+use Dancer2::Core::Types;
+use Dancer2::Core::Request;
+use Dancer2::Core::Response;
+
+has apps => (
+ is => 'rw',
+ isa => ArrayRef,
+ default => sub { [] },
+);
+
+has apps_psgi => (
+ is => 'ro',
+ isa => ArrayRef,
+ lazy => 1,
+ builder => '_build_apps_psgi',
+);
+
+sub _build_apps_psgi {
+ my $self = shift;
+ return [ map +( $_->name, $_->to_app ), @{ $self->apps } ];
+}
+
+sub dispatch {
+ my ( $self, $env ) = @_;
+ my @apps = @{ $self->apps_psgi };
+
+ DISPATCH: while (1) {
+ for ( my $i = 0; $i < @apps; $i += 2 ) {
+ my ( $app_name, $app ) = @apps[ $i, $i + 1 ];
+
+ my $response = $app->($env);
+
+ # check for an internal request
+ delete Dancer2->runner->{'internal_forward'}
+ and next DISPATCH;
+
+ # the app raised a flag saying it couldn't match anything
+ # which is different than "I matched and it's a 404"
+ delete Dancer2->runner->{'internal_404'}
+ or do {
+ delete Dancer2->runner->{'internal_request'};
+ return $response;
+ };
+ }
+
+ # don't run anymore
+ delete Dancer2->runner->{'internal_request'};
+ last;
+ } # while
+
+ # a 404 on all apps, using the first app
+ my $default_app = $self->apps->[0];
+ my $request = $default_app->build_request($env);
+ return $default_app->response_not_found($request)->to_psgi;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Dispatcher - Class for dispatching request to the appropriate route handler
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ use Dancer2::Core::Dispatcher;
+
+ # Create an instance of dispatcher
+ my $dispatcher = Dancer2::Core::Dispatcher->new( apps => [$app] );
+
+ # Dispatch a request
+ my $resp = $dispatcher->dispatch($env)->to_psgi;
+
+ # Capture internal error of a response (if any) after a dispatch
+ $dispatcher->response_internal_error($app, $error);
+
+ # Capture response not found for an application the after dispatch
+ $dispatcher->response_not_found($env);
+
+=head1 ATTRIBUTES
+
+=head2 apps
+
+The apps is an array reference to L<Dancer2::Core::App>.
+
+=head2 default_content_type
+
+The default_content_type is a string which represents the context of the
+request. This attribute is read-only.
+
+=head1 METHODS
+
+=head2 dispatch
+
+The C<dispatch> method accepts the list of applications, hash reference for
+the B<env> attribute of L<Dancer2::Core::Request> and optionally the request
+object and an env as input arguments.
+
+C<dispatch> returns a response object of L<Dancer2::Core::Response>.
+
+Any before hook and matched route code is wrapped to allow DSL keywords such
+as forward and redirect to short-circuit remaining code, returning across
+multiple stack frames without having to throw an exception.
+
+=head2 response_internal_error
+
+The C<response_internal_error> takes as input the list of applications and
+a variable error and returns an object of L<Dancer2::Core::Error>.
+
+=head2 response_not_found
+
+The C<response_not_found> consumes as input the list of applications and an
+object of type L<Dancer2::Core::App> and returns an object
+L<Dancer2::Core::Error>.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Error.pm b/lib/Dancer2/Core/Error.pm
new file mode 100644
index 00000000..7d9f667d
--- /dev/null
+++ b/lib/Dancer2/Core/Error.pm
@@ -0,0 +1,596 @@
+package Dancer2::Core::Error;
+# ABSTRACT: Class representing fatal errors
+$Dancer2::Core::Error::VERSION = '0.300000';
+use Moo;
+use Carp;
+use Dancer2::Core::Types;
+use Dancer2::Core::HTTP;
+use Data::Dumper;
+use Dancer2::FileUtils qw/path open_file/;
+use Sub::Quote;
+use Module::Runtime 'require_module';
+use Ref::Util qw< is_hashref >;
+use Clone qw(clone);
+
+has app => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::App'],
+ predicate => 'has_app',
+);
+
+has show_errors => (
+ is => 'ro',
+ isa => Bool,
+ default => sub {
+ my $self = shift;
+
+ $self->has_app
+ and return $self->app->setting('show_errors');
+ },
+);
+
+has charset => (
+ is => 'ro',
+ isa => Str,
+ default => sub {'UTF-8'},
+);
+
+has type => (
+ is => 'ro',
+ isa => Str,
+ default => sub {'Runtime Error'},
+);
+
+has title => (
+ is => 'ro',
+ isa => Str,
+ lazy => 1,
+ builder => '_build_title',
+);
+
+sub _build_title {
+ my ($self) = @_;
+ my $title = 'Error ' . $self->status;
+ if ( my $msg = Dancer2::Core::HTTP->status_message($self->status) ) {
+ $title .= ' - ' . $msg;
+ }
+
+ return $title;
+}
+
+has template => (
+ is => 'ro',
+ lazy => 1,
+ builder => '_build_error_template',
+);
+
+sub _build_error_template {
+ my ($self) = @_;
+
+ # look for a template named after the status number.
+ # E.g.: views/404.tt for a TT template
+ my $engine = $self->app->template_engine;
+ return $self->status
+ if $engine->pathname_exists( $engine->view_pathname( $self->status ) );
+
+ return;
+}
+
+has static_page => (
+ is => 'ro',
+ lazy => 1,
+ builder => '_build_static_page',
+);
+
+sub _build_static_page {
+ my ($self) = @_;
+
+ # TODO there must be a better way to get it
+ my $public_dir = $ENV{DANCER_PUBLIC}
+ || ( $self->has_app && $self->app->config->{public_dir} );
+
+ my $filename = sprintf "%s/%d.html", $public_dir, $self->status;
+
+ open my $fh, '<', $filename or return;
+
+ local $/ = undef; # slurp time
+
+ return <$fh>;
+}
+
+sub default_error_page {
+ my $self = shift;
+
+ require_module('Template::Tiny');
+
+ my $uri_base = $self->has_app && $self->app->has_request ?
+ $self->app->request->uri_base : '';
+
+ # GH#1001 stack trace if show_errors is true and this is a 'server' error (5xx)
+ my $show_fullmsg = $self->show_errors && $self->status =~ /^5/;
+ my $opts = {
+ title => $self->title,
+ charset => $self->charset,
+ content => $show_fullmsg ? $self->full_message : _html_encode($self->message) || 'Wooops, something went wrong',
+ version => Dancer2->VERSION,
+ uri_base => $uri_base,
+ };
+
+ Template::Tiny->new->process( \<<"END_TEMPLATE", $opts, \my $output );
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="[% charset %]">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+ <title>[% title %]</title>
+ <link rel="stylesheet" href="[% uri_base %]/css/error.css">
+</head>
+<body>
+<h1>[% title %]</h1>
+<div id="content">
+[% content %]
+</div>
+<div id="footer">
+Powered by <a href="http://perldancer.org/">Dancer2</a> [% version %]
+</div>
+</body>
+</html>
+END_TEMPLATE
+
+ return $output;
+}
+
+has status => (
+ is => 'ro',
+ default => sub {500},
+ isa => Num,
+);
+
+has message => (
+ is => 'ro',
+ isa => Str,
+ lazy => 1,
+ default => sub { '' },
+);
+
+sub full_message {
+ my ($self) = @_;
+ my $html_output = "<h2>" . $self->type . "</h2>";
+ $html_output .= $self->backtrace;
+ $html_output .= $self->environment;
+ return $html_output;
+}
+
+has serializer => (
+ is => 'ro',
+ isa => Maybe[ConsumerOf['Dancer2::Core::Role::Serializer']],
+ builder => '_build_serializer',
+);
+
+sub _build_serializer {
+ my ($self) = @_;
+
+ $self->has_app && $self->app->has_serializer_engine
+ and return $self->app->serializer_engine;
+
+ return;
+}
+
+sub BUILD {
+ my ($self) = @_;
+
+ $self->has_app &&
+ $self->app->execute_hook( 'core.error.init', $self );
+}
+
+has exception => (
+ is => 'ro',
+ isa => Str,
+ predicate => 1,
+ coerce => sub {
+ # Until we properly support exception objects, we shouldn't barf on
+ # them because that hides the actual error, if object overloads "",
+ # which most exception objects do, this will result in a nicer string.
+ # other references will produce a meaningless error, but that is
+ # better than a meaningless stacktrace
+ return "$_[0]"
+ }
+);
+
+has response => (
+ is => 'rw',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ my $serializer = $self->serializer;
+ # include server tokens in response ?
+ my $no_server_tokens = $self->has_app
+ ? $self->app->config->{'no_server_tokens'}
+ : defined $ENV{DANCER_NO_SERVER_TOKENS}
+ ? $ENV{DANCER_NO_SERVER_TOKENS}
+ : 0;
+ return Dancer2::Core::Response->new(
+ server_tokens => !$no_server_tokens,
+ ( serializer => $serializer )x!! $serializer
+ );
+ }
+);
+
+has content_type => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ $self->serializer
+ ? $self->serializer->content_type
+ : 'text/html'
+ },
+);
+
+has content => (
+ is => 'ro',
+ lazy => 1,
+ builder => '_build_content',
+);
+
+sub _build_content {
+ my $self = shift;
+
+ # return a hashref if a serializer is available
+ if ( $self->serializer ) {
+ my $content = {
+ message => $self->message,
+ title => $self->title,
+ status => $self->status,
+ };
+ $content->{exception} = $self->exception
+ if $self->has_exception;
+ return $content;
+ }
+
+ # otherwise we check for a template, for a static file,
+ # for configured error_template, and, if all else fails,
+ # the default error page
+ if ( $self->has_app and $self->template ) {
+ # Render the template using apps' template engine.
+ # This may well be what caused the initial error, in which
+ # case we fall back to static page if any error was thrown.
+ # Note: this calls before/after render hooks.
+ my $content = eval {
+ $self->app->template(
+ $self->template,
+ { title => $self->title,
+ content => $self->message,
+ exception => $self->exception,
+ status => $self->status,
+ }
+ );
+ };
+ $@ && $self->app->engine('logger')->log( warning => $@ );
+
+ # return rendered content unless there was an error.
+ return $content if defined $content;
+ }
+
+ # It doesn't make sense to return a static page for a 500 if show_errors is on
+ if ( !($self->show_errors && $self->status eq '500') ) {
+ if ( my $content = $self->static_page ) {
+ return $content;
+ }
+ }
+
+ if ($self->has_app && $self->app->config->{error_template}) {
+ my $content = eval {
+ $self->app->template(
+ $self->app->config->{error_template},
+ { title => $self->title,
+ content => $self->message,
+ exception => $self->exception,
+ status => $self->status,
+ }
+ );
+ };
+ $@ && $self->app->engine('logger')->log( warning => $@ );
+
+ # return rendered content unless there was an error.
+ return $content if defined $content;
+ }
+
+ return $self->default_error_page;
+}
+
+sub throw {
+ my $self = shift;
+ $self->response(shift) if @_;
+
+ $self->response
+ or croak "error has no response to throw at";
+
+ $self->has_app &&
+ $self->app->execute_hook( 'core.error.before', $self );
+
+ my $message = $self->content;
+
+ $self->response->status( $self->status );
+ $self->response->content_type( $self->content_type );
+ $self->response->content($message);
+
+ $self->has_app &&
+ $self->app->execute_hook('core.error.after', $self->response);
+
+ $self->response->is_halted(1);
+ return $self->response;
+}
+
+sub backtrace {
+ my ($self) = @_;
+
+ my $message = $self->message;
+ if ($self->exception) {
+ $message .= "\n" if $message;
+ $message .= $self->exception;
+ }
+ $message ||= 'Wooops, something went wrong';
+
+ my $html = '<pre class="error">' . _html_encode($message) . "</pre>\n";
+
+ # the default perl warning/error pattern
+ my ($file, $line) = $message =~ /at (\S+) line (\d+)/;
+ # the Devel::SimpleTrace pattern
+ ($file, $line) = $message =~ /at.*\((\S+):(\d+)\)/ unless $file and $line;
+
+ # no file/line found, cannot open a file for context
+ return $html unless $file and $line;
+
+ # file and line are located, let's read the source Luke!
+ my $fh = eval { open_file('<', $file) } or return $html;
+ my @lines = <$fh>;
+ close $fh;
+
+ $html .= qq|<div class="title">$file around line $line</div>|;
+
+ # get 5 lines of context
+ my $start = $line - 5 > 1 ? $line - 5 : 1;
+ my $stop = $line + 5 < @lines ? $line + 5 : @lines;
+
+ $html .= qq|<pre class="content"><table class="context">\n|;
+ for my $l ($start .. $stop) {
+ chomp $lines[$l - 1];
+
+ $html .= $l == $line ? '<tr class="errline">' : '<tr>';
+ $html .= "<th>$l</th><td>" . _html_encode($lines[$l - 1]) . "</td></tr>\n";
+ }
+ $html .= "</table></pre>\n";
+
+ return $html;
+}
+
+sub dumper {
+ my $obj = shift;
+
+ # Take a copy of the data, so we can mask sensitive-looking stuff:
+ my $data = clone($obj);
+ my $censored = _censor( $data );
+
+ #use Data::Dumper;
+ my $dd = Data::Dumper->new( [ $data ] );
+ my $hash_separator = ' @@!%,+$$#._(-- '; # Very unlikely string to exist already
+ my $prefix_padding = ' #+#+@%.,$_-!(( '; # Very unlikely string to exist already
+ $dd->Terse(1)->Quotekeys(0)->Indent(1)->Sortkeys(1)->Pair($hash_separator)->Pad($prefix_padding);
+ my $content = _html_encode( $dd->Dump );
+ $content =~ s/^.+//; # Remove the first line
+ $content =~ s/\n.+$//; # Remove the last line
+ $content =~ s/^\Q$prefix_padding\E //gm; # Remove the padding
+ $content =~ s{^(\s*)(.+)\Q$hash_separator}{$1<span class="key">$2</span> =&gt; }gm;
+ if ($censored) {
+ $content
+ .= "\n\nNote: Values of $censored sensitive-looking keys hidden\n";
+ }
+ return $content;
+}
+
+sub environment {
+ my ($self) = @_;
+
+ my $stack = $self->get_caller;
+ my $settings = $self->has_app && $self->app->settings;
+ my $session = $self->has_app && $self->app->_has_session && $self->app->session->data;
+ my $env = $self->has_app && $self->app->has_request && $self->app->request->env;
+
+ # Get a sanitised dump of the settings, session and environment
+ $_ = $_ ? dumper($_) : '<i>undefined</i>' for $settings, $session, $env;
+
+ return <<"END_HTML";
+<div class="title">Stack</div><pre class="content">$stack</pre>
+<div class="title">Settings</div><pre class="content">$settings</pre>
+<div class="title">Session</div><pre class="content">$session</pre>
+<div class="title">Environment</div><pre class="content">$env</pre>
+END_HTML
+}
+
+sub get_caller {
+ my ($self) = @_;
+ my @stack;
+
+ my $deepness = 0;
+ while ( my ( $package, $file, $line ) = caller( $deepness++ ) ) {
+ push @stack, "$package in $file l. $line";
+ }
+
+ return join( "\n", reverse(@stack) );
+}
+
+# private
+
+# Given a hashref, censor anything that looks sensitive. Returns number of
+# items which were "censored".
+
+sub _censor {
+ my $hash = shift;
+ my $visited = shift || {};
+
+ unless ( $hash && is_hashref($hash) ) {
+ carp "_censor given incorrect input: $hash";
+ return;
+ }
+
+ my $censored = 0;
+ for my $key ( keys %$hash ) {
+ if ( is_hashref( $hash->{$key} ) ) {
+ if (!$visited->{ $hash->{$key} }) {
+ # mark the new ref as visited
+ $visited->{ $hash->{$key} } = 1;
+
+ $censored += _censor( $hash->{$key}, $visited );
+ }
+ }
+ elsif ( $key =~ /(pass|card?num|pan|secret)/i ) {
+ $hash->{$key} = "Hidden (looks potentially sensitive)";
+ $censored++;
+ }
+ }
+
+ return $censored;
+}
+
+# Replaces the entities that are illegal in (X)HTML.
+sub _html_encode {
+ my $value = shift;
+
+ return if !defined $value;
+
+ $value =~ s/&/&amp;/g;
+ $value =~ s/</&lt;/g;
+ $value =~ s/>/&gt;/g;
+ $value =~ s/'/&#39;/g;
+ $value =~ s/"/&quot;/g;
+
+ return $value;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Error - Class representing fatal errors
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ # taken from send_file:
+ use Dancer2::Core::Error;
+
+ my $error = Dancer2::Core::Error->new(
+ status => 404,
+ message => "No such file: `$path'"
+ );
+
+ Dancer2::Core::Response->set($error->render);
+
+=head1 DESCRIPTION
+
+With Dancer2::Core::Error you can throw reasonable-looking errors to the user
+instead of crashing the application and filling up the logs.
+
+This is usually used in debugging environments, and it's what Dancer2 uses as
+well under debugging to catch errors and show them on screen.
+
+=head1 ATTRIBUTES
+
+=head2 show_errors
+
+=head2 charset
+
+=head2 type
+
+The error type.
+
+=head2 title
+
+The title of the error page.
+
+This is only an attribute getter, you'll have to set it at C<new>.
+
+=head2 status
+
+The status that caused the error.
+
+This is only an attribute getter, you'll have to set it at C<new>.
+
+=head2 message
+
+The message of the error page.
+
+=head1 METHODS
+
+=head2 my $error=new Dancer2::Core::Error(status => 404, message => "No such file: `$path'");
+
+Create a new Dancer2::Core::Error object. For available arguments see ATTRIBUTES.
+
+=head2 supported_hooks ();
+
+=head2 throw($response)
+
+Populates the content of the response with the error's information.
+If I<$response> is not given, acts on the I<app>
+attribute's response.
+
+=head2 backtrace
+
+Show the surrounding lines of context at the line where the error was thrown.
+
+This method tries to find out where the error appeared according to the actual
+error message (using the C<message> attribute) and tries to parse it (supporting
+the regular/default Perl warning or error pattern and the L<Devel::SimpleTrace>
+output) and then returns an error-highlighted C<message>.
+
+=head2 environment
+
+A main function to render environment information: the caller (using
+C<get_caller>), the settings and environment (using C<dumper>) and more.
+
+=head2 get_caller
+
+Creates a stack trace of callers.
+
+=head1 FUNCTIONS
+
+=head2 _censor
+
+An private function that tries to censor out content which should be protected.
+
+C<dumper> calls this method to censor things like passwords and such.
+
+=head2 my $string=_html_encode ($string);
+
+Private function that replaces illegal entities in (X)HTML with their
+escaped representations.
+
+html_encode() doesn't do any UTF black magic.
+
+=head2 dumper
+
+This uses L<Data::Dumper> to create nice content output with a few predefined
+options.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Factory.pm b/lib/Dancer2/Core/Factory.pm
new file mode 100644
index 00000000..02874f00
--- /dev/null
+++ b/lib/Dancer2/Core/Factory.pm
@@ -0,0 +1,49 @@
+package Dancer2::Core::Factory;
+# ABSTRACT: Instantiate components by type and name
+$Dancer2::Core::Factory::VERSION = '0.300000';
+use Moo;
+use Dancer2::Core;
+use Module::Runtime 'use_module';
+use Carp 'croak';
+
+sub create {
+ my ( $class, $type, $name, %options ) = @_;
+
+ $type = Dancer2::Core::camelize($type);
+ $name = Dancer2::Core::camelize($name);
+ my $component_class = "Dancer2::${type}::${name}";
+
+ eval { use_module($component_class); 1; }
+ or croak "Unable to load class for $type component $name: $@";
+
+ return $component_class->new(%options);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Factory - Instantiate components by type and name
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/HTTP.pm b/lib/Dancer2/Core/HTTP.pm
new file mode 100644
index 00000000..ed055fca
--- /dev/null
+++ b/lib/Dancer2/Core/HTTP.pm
@@ -0,0 +1,204 @@
+# ABSTRACT: helper for rendering HTTP status codes for Dancer2
+
+package Dancer2::Core::HTTP;
+$Dancer2::Core::HTTP::VERSION = '0.300000';
+use strict;
+use warnings;
+
+use List::Util qw/ pairmap pairgrep /;
+
+my $HTTP_CODES = {
+
+ # informational
+ 100 => 'Continue', # only on HTTP 1.1
+ 101 => 'Switching Protocols', # only on HTTP 1.1
+ 102 => 'Processing', # WebDAV; RFC 2518
+
+ # processed
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information', # only on HTTP 1.1
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status', # WebDAV; RFC 4918
+ 208 => 'Already Reported', # WebDAV; RFC 5842
+ # 226 => 'IM Used' # RFC 3229
+
+ # redirections
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other', # only on HTTP 1.1
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy', # only on HTTP 1.1
+ 306 => 'Switch Proxy',
+ 307 => 'Temporary Redirect', # only on HTTP 1.1
+ # 308 => 'Permanent Redirect' # approved as experimental RFC
+
+ # problems with request
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 418 => "I'm a teapot", # RFC 2324
+ # 419 => 'Authentication Timeout', # not in RFC 2616
+ 420 => 'Enhance Your Calm',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency', # Also used for 'Method Failure'
+ 425 => 'Unordered Collection',
+ 426 => 'Upgrade Required',
+ 428 => 'Precondition Required',
+ 429 => 'Too Many Requests',
+ 431 => 'Request Header Fields Too Large',
+ 444 => 'No Response',
+ 449 => 'Retry With',
+ 450 => 'Blocked by Windows Parental Controls',
+ 451 => 'Unavailable For Legal Reasons',
+ 494 => 'Request Header Too Large',
+ 495 => 'Cert Error',
+ 496 => 'No Cert',
+ 497 => 'HTTP to HTTPS',
+ 499 => 'Client Closed Request',
+
+ # problems with server
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 508 => 'Loop Detected',
+ 509 => 'Bandwidth Limit Exceeded',
+ 510 => 'Not Extended',
+ 511 => 'Network Authentication Required',
+ 598 => 'Network read timeout error',
+ 599 => 'Network connect timeout error',
+};
+
+$HTTP_CODES = {
+ %$HTTP_CODES,
+ ( reverse %$HTTP_CODES ),
+ pairmap { join( '_', split /\W/, lc $a ) => $b } reverse %$HTTP_CODES
+};
+
+$HTTP_CODES->{error} = $HTTP_CODES->{internal_server_error};
+
+sub status {
+ my ( $class, $status ) = @_;
+ return if ! defined $status;
+ return $status if $status =~ /^\d+$/;
+ if ( exists $HTTP_CODES->{$status} ) {
+ return $HTTP_CODES->{$status};
+ }
+ return;
+}
+
+sub status_message {
+ my ( $class, $status ) = @_;
+ return if ! defined $status;
+ my $code = $class->status($status);
+ return if ! defined $code || ! exists $HTTP_CODES->{$code};
+ return $HTTP_CODES->{ $code };
+}
+
+sub status_mapping {
+ pairgrep { $b =~ /^\d+$/ and $a !~ /_/ } %$HTTP_CODES;
+}
+
+sub code_mapping {
+ my @result = reverse status_mapping();
+ return @result;
+}
+
+sub all_mappings { %$HTTP_CODES }
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::HTTP - helper for rendering HTTP status codes for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 FUNCTIONS
+
+=head2 status(status_code)
+
+ Dancer2::Core::HTTP->status(200); # returns 200
+
+ Dancer2::Core::HTTP->status('Not Found'); # returns 404
+
+ Dancer2::Core::HTTP->status('bad_request'); # 400
+
+Returns a HTTP status code. If given an integer, it will return the value it
+received, else it will try to find the appropriate alias and return the correct
+status.
+
+=head2 status_message(status_code)
+
+ Dancer2::Core::HTTP->status_message(200); # returns 'OK'
+
+ Dancer2::Core::HTTP->status_message('error'); # returns 'Internal Server Error'
+
+Returns the HTTP status message for the given status code.
+
+=head2 status_mapping()
+
+ my %table = Dancer2::Core::HTTP->status_mapping;
+ # returns ( 'Ok' => 200, 'Created' => 201, ... )
+
+Returns the full table of status -> code mappings.
+
+=head2 code_mapping()
+
+ my %table = Dancer2::Core::HTTP->code_mapping;
+ # returns ( 200 => 'Ok', 201 => 'Created', ... )
+
+Returns the full table of code -> status mappings.
+
+=head2 all_mappings()
+
+ my %table = Dancer2::Core::HTTP->all_mappings;
+ # returns ( 418 => 'I'm a teapot', "I'm a teapot' => 418, 'i_m_a_teapot' => 418 )
+
+Returns the code-to-status, status-to-code and underscore-groomed status-to-code mappings
+all mashed up in a single table. Mostly for internal uses.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Hook.pm b/lib/Dancer2/Core/Hook.pm
new file mode 100644
index 00000000..a97e43b7
--- /dev/null
+++ b/lib/Dancer2/Core/Hook.pm
@@ -0,0 +1,112 @@
+package Dancer2::Core::Hook;
+# ABSTRACT: Manipulate hooks with Dancer2
+$Dancer2::Core::Hook::VERSION = '0.300000';
+use Moo;
+use Dancer2::Core::Types;
+use Carp;
+
+has name => (
+ is => 'rw',
+ isa => Str,
+ required => 1,
+ coerce => sub {
+ my ($hook_name) = @_;
+
+ # XXX at the moment, we have a filer position named "before_template".
+ # this one is renamed "before_template_render", so we need to alias it.
+ # maybe we need to deprecate 'before_template' to enforce the use
+ # of 'hook before_template_render => sub {}' ?
+ $hook_name = 'before_template_render'
+ if $hook_name eq 'before_template';
+ return $hook_name;
+ },
+);
+
+has code => (
+ is => 'ro',
+ isa => CodeRef,
+ required => 1,
+ coerce => sub {
+ my ($hook) = @_;
+ sub {
+ my $res;
+ eval { $res = $hook->(@_) };
+ croak "Hook error: $@" if $@;
+ return $res;
+ };
+ },
+);
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Hook - Manipulate hooks with Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ # inside a plugin
+ use Dancer2::Core::Hook;
+ Dancer2::Core::Hook->register_hooks_name(qw/before_auth after_auth/);
+
+=head1 METHODS
+
+=head2 register_hook ($hook_name, [$properties], $code)
+
+ hook 'before', {apps => ['main']}, sub {...};
+
+ hook 'before' => sub {...};
+
+Attaches a hook at some point, with a possible list of properties.
+
+Currently supported properties:
+
+=over 4
+
+=item apps
+
+ an array reference containing apps name
+
+=back
+
+=head2 register_hooks_name
+
+Add a new hook name, so application developers can insert some code at this point.
+
+ package My::Dancer2::Plugin;
+ Dancer2::Core::Hook->instance->register_hooks_name(qw/before_auth after_auth/);
+
+=head2 execute_hook
+
+Execute a hooks
+
+=head2 get_hooks_for
+
+Returns the list of coderef registered for a given position
+
+=head2 hook_is_registered
+
+Test if a hook with this name has already been registered.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/MIME.pm b/lib/Dancer2/Core/MIME.pm
new file mode 100644
index 00000000..ca4a70a6
--- /dev/null
+++ b/lib/Dancer2/Core/MIME.pm
@@ -0,0 +1,177 @@
+# ABSTRACT: Class to ease manipulation of MIME types
+
+package Dancer2::Core::MIME;
+$Dancer2::Core::MIME::VERSION = '0.300000';
+use Moo;
+
+use Plack::MIME;
+use Dancer2::Core::Types;
+use Module::Runtime 'require_module';
+
+# Initialise MIME::Types at compile time, to ensure it's done before
+# the fork in a preforking webserver like mod_perl or Starman. Not
+# doing this leads to all MIME types being returned as "text/plain",
+# as MIME::Types fails to load its mappings from the DATA handle. See
+# t/04_static_file/003_mime_types_reinit.t and GH#136.
+BEGIN {
+ if ( eval { require_module('MIME::Types'); 1; } ) {
+ my $mime_types = MIME::Types->new(only_complete => 1);
+ Plack::MIME->set_fallback(
+ sub {
+ $mime_types->mimeTypeOf($_[0])
+ }
+ );
+ }
+}
+
+has custom_types => (
+ is => 'ro',
+ isa => HashRef,
+ default => sub { +{} },
+);
+
+has default => (
+ is => 'rw',
+ isa => Str,
+ builder => "reset_default",
+);
+
+sub reset_default {
+ my ($self) = @_;
+ $self->default("application/data");
+}
+
+sub add_type {
+ my ( $self, $name, $type ) = @_;
+ $self->custom_types->{$name} = $type;
+ return;
+}
+
+sub add_alias {
+ my ( $self, $alias, $orig ) = @_;
+ my $type = $self->for_name($orig);
+ $self->add_type( $alias, $type );
+ return $type;
+}
+
+sub for_file {
+ my ( $self, $filename ) = @_;
+ my ($ext) = $filename =~ /\.([^.]+)$/;
+ return $self->default unless $ext;
+ return $self->for_name($ext);
+}
+
+sub name_or_type {
+ my ( $self, $name ) = @_;
+
+ return $name if $name =~ m{/}; # probably a mime type
+ return $self->for_name($name);
+}
+
+sub for_name {
+ my ( $self, $name ) = @_;
+
+ return
+ $self->custom_types->{ lc $name }
+ || Plack::MIME->mime_type( lc ".$name" )
+ || $self->default;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::MIME - Class to ease manipulation of MIME types
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ use Dancer2::Core::MIME;
+
+ my $mime = Dancer2::Core::MIME->new();
+
+ # get mime type for an alias
+ my $type = $mime->for_name('css');
+
+ # set a new mime type
+ my $type = $mime->add_type( foo => 'text/foo' );
+
+ # set a mime type alias
+ my $alias = $mime->add_alias( f => 'foo' );
+
+ # get mime type for a file (based on extension)
+ my $file = $mime->for_file( "foo.bar" );
+
+ # set the $thing into a content $type.
+ my $type = $mime->name_or_type($thing);
+
+ # get current defined default mime type
+ my $type = $mime->default;
+
+ # set the default mime type using config.yml
+ # or using the set keyword
+ set default_mime_type => 'text/plain';
+
+=head1 DESCRIPTION
+
+Dancer2::Core::MIME is a thin wrapper around L<MIME::Types> providing helpful
+methods for MIME handling.
+
+=head1 ATTRIBUTES
+
+=head2 custom_types
+
+Custom user-defined MIME types that are added the with C<add_type>.
+
+=head2 default
+
+Default MIME type defined by MIME::Types, set to: B<application/data>.
+
+=head1 METHODS
+
+=head2 reset_default
+
+This method resets C<mime_type> to the default type.
+
+=head2 add_type
+
+This method adds the new MIME type.
+
+=head2 add_alias
+
+The C<add_alias> sets a MIME type alias.
+
+=head2 for_name
+
+The method C<for_name> gets MIME type for an alias.
+
+=head2 for_file
+
+This method gets MIME type for a file based on extension.
+
+=head2 name_or_type
+
+This method sets the customized MIME name or default MIME type into a content
+type.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Request.pm b/lib/Dancer2/Core/Request.pm
new file mode 100644
index 00000000..f789f13c
--- /dev/null
+++ b/lib/Dancer2/Core/Request.pm
@@ -0,0 +1,1148 @@
+package Dancer2::Core::Request;
+# ABSTRACT: Interface for accessing incoming requests
+$Dancer2::Core::Request::VERSION = '0.300000';
+use strict;
+use warnings;
+use parent 'Plack::Request';
+
+use Carp;
+use Encode;
+use URI;
+use URI::Escape;
+use Safe::Isa;
+use Hash::MultiValue;
+use Module::Runtime 'require_module';
+use Ref::Util qw< is_ref is_arrayref is_hashref >;
+
+use Dancer2::Core::Types;
+use Dancer2::Core::Request::Upload;
+use Dancer2::Core::Cookie;
+
+# add an attribute for each HTTP_* variables
+# (HOST is managed manually)
+my @http_env_keys = (qw/
+ accept_charset
+ accept_encoding
+ accept_language
+ connection
+ keep_alive
+ x_requested_with
+/);
+
+# apparently you can't eval core functions
+sub accept { $_[0]->env->{'HTTP_ACCEPT'} }
+
+eval << "_EVAL" or die $@ for @http_env_keys; ## no critic
+sub $_ { \$_[0]->env->{ 'HTTP_' . ( uc "$_" ) } }
+1;
+_EVAL
+
+# check presence of XS module to speedup request
+our $XS_URL_DECODE = eval { require_module('URL::Encode::XS'); 1; };
+our $XS_PARSE_QUERY_STRING = eval { require_module('CGI::Deurl::XS'); 1; };
+our $XS_HTTP_COOKIES = eval { require_module('HTTP::XSCookies'); 1; };
+
+our $_id = 0;
+
+# self->new( env => {}, serializer => $s, is_behind_proxy => 0|1 )
+sub new {
+ my ( $class, @args ) = @_;
+
+ # even sized list
+ @args % 2 == 0
+ or croak 'Must provide even sized list';
+
+ my %opts = @args;
+ my $env = $opts{'env'};
+
+ my $self = $class->SUPER::new($env);
+
+ if ( my $s = $opts{'serializer'} ) {
+ $s->$_does('Dancer2::Core::Role::Serializer')
+ or croak 'Serializer provided not a Serializer object';
+
+ $self->{'serializer'} = $s;
+ }
+
+ # additionally supported attributes
+ $self->{'id'} = ++$_id;
+ $self->{'vars'} = {};
+ $self->{'is_behind_proxy'} = !!$opts{'is_behind_proxy'};
+
+ $opts{'body_params'}
+ and $self->{'_body_params'} = $opts{'body_params'};
+
+ # Deserialize/parse body for HMV
+ $self->data;
+ $self->_build_uploads();
+
+ return $self;
+}
+
+# a buffer for per-request variables
+sub vars { $_[0]->{'vars'} }
+
+sub var {
+ my $self = shift;
+ @_ == 2
+ ? $self->vars->{ $_[0] } = $_[1]
+ : $self->vars->{ $_[0] };
+}
+
+# I don't like this. I know send_file uses this and I wonder
+# if we can remove it.
+# -- Sawyer
+sub set_path_info { $_[0]->env->{'PATH_INFO'} = $_[1] }
+
+# XXX: incompatible with Plack::Request
+sub body { $_[0]->raw_body }
+
+sub id { $_id }
+
+# Private 'read-only' attributes for request params. See the params()
+# method for the public interface.
+#
+# _body_params, _query_params and _route_params have setter methods that
+# decode byte string to characters before setting; If you know you have
+# decoded (character) params, such as output from a deserializer, you can
+# set these directly in the request object hash to avoid the decode op.
+sub _params { $_[0]->{'_params'} ||= $_[0]->_build_params }
+
+sub _has_params { defined $_[0]->{'_params'} }
+
+sub _body_params { $_[0]->{'_body_params'} ||= $_[0]->body_parameters->as_hashref_mixed }
+
+sub _query_params { $_[0]->{'_query_params'} }
+
+sub _set_query_params {
+ my ( $self, $params ) = @_;
+ $self->{_query_params} = _decode( $params );
+}
+
+sub _route_params { $_[0]->{'_route_params'} ||= {} }
+
+sub _set_route_params {
+ my ( $self, $params ) = @_;
+ $self->{_route_params} = _decode( $params );
+ $self->_build_params();
+}
+
+# XXX: incompatible with Plack::Request
+sub uploads { $_[0]->{'uploads'} }
+
+sub is_behind_proxy { $_[0]->{'is_behind_proxy'} || 0 }
+
+sub host {
+ my ($self) = @_;
+
+ if ( $self->is_behind_proxy and exists $self->env->{'HTTP_X_FORWARDED_HOST'} ) {
+ my @hosts = split /\s*,\s*/, $self->env->{'HTTP_X_FORWARDED_HOST'}, 2;
+ return $hosts[0];
+ } else {
+ return $self->env->{'HTTP_HOST'};
+ }
+}
+
+# aliases, kept for backward compat
+sub agent { shift->user_agent }
+sub remote_address { shift->address }
+sub forwarded_for_address { shift->env->{'HTTP_X_FORWARDED_FOR'} }
+sub forwarded_host { shift->env->{'HTTP_X_FORWARDED_HOST'} }
+
+# there are two options
+sub forwarded_protocol {
+ $_[0]->env->{'HTTP_X_FORWARDED_PROTO'} ||
+ $_[0]->env->{'HTTP_X_FORWARDED_PROTOCOL'} ||
+ $_[0]->env->{'HTTP_FORWARDED_PROTO'}
+}
+
+sub scheme {
+ my ($self) = @_;
+ my $scheme = $self->is_behind_proxy
+ ? $self->forwarded_protocol
+ : '';
+
+ return $scheme || $self->env->{'psgi.url_scheme'};
+}
+
+sub serializer { $_[0]->{'serializer'} }
+
+sub data { $_[0]->{'data'} ||= $_[0]->deserialize() }
+
+sub deserialize {
+ my $self = shift;
+
+ my $serializer = $self->serializer
+ or return;
+
+ # The latest draft of the RFC does not forbid DELETE to have content,
+ # rather the behaviour is undefined. Take the most lenient route and
+ # deserialize any content on delete as well.
+ return
+ unless grep { $self->method eq $_ } qw/ PUT POST PATCH DELETE /;
+
+ # try to deserialize
+ my $body = $self->body;
+
+ $body && length $body > 0
+ or return;
+
+ # Catch serializer fails - which is tricky as Role::Serializer
+ # wraps the deserializaion in an eval and returns undef.
+ # We want to generate a 500 error on serialization fail (Ref #794)
+ # to achieve that, override the log callback so we can catch a signal
+ # that it failed. This is messy (messes with serializer internals), but
+ # "works".
+ my $serializer_fail;
+ my $serializer_log_cb = $serializer->log_cb;
+ local $serializer->{log_cb} = sub {
+ $serializer_fail = $_[1];
+ $serializer_log_cb->(@_);
+ };
+ # work-around to resolve a chicken-and-egg issue when instantiating a
+ # request object; the serializer needs that request object to deserialize
+ # the body params.
+ Scalar::Util::weaken( my $request = $self );
+ $self->serializer->has_request || $self->serializer->set_request($request);
+ my $data = $serializer->deserialize($body);
+ die $serializer_fail if $serializer_fail;
+
+ # Set _body_params directly rather than using the setter. Deserializiation
+ # returns characters and skipping the decode op in the setter ensures
+ # that numerical data "stays" numerical; decoding an SV that is an IV
+ # converts that to a PVIV. Some serializers are picky (JSON)..
+ $self->{_body_params} = $data;
+
+ # Set body parameters (decoded HMV)
+ $self->{'body_parameters'} =
+ Hash::MultiValue->from_mixed( is_hashref($data) ? %$data : () );
+
+ return $data;
+}
+
+sub uri { $_[0]->request_uri }
+
+sub is_head { $_[0]->method eq 'HEAD' }
+sub is_post { $_[0]->method eq 'POST' }
+sub is_get { $_[0]->method eq 'GET' }
+sub is_put { $_[0]->method eq 'PUT' }
+sub is_delete { $_[0]->method eq 'DELETE' }
+sub is_patch { $_[0]->method eq 'PATCH' }
+sub is_options { $_[0]->method eq 'OPTIONS' }
+
+# public interface compat with CGI.pm objects
+sub request_method { $_[0]->method }
+sub input_handle { $_[0]->env->{'psgi.input'} }
+
+sub to_string {
+ my ($self) = @_;
+ return "[#" . $self->id . "] " . $self->method . " " . $self->path;
+}
+
+sub base {
+ my $self = shift;
+ my $uri = $self->_common_uri;
+
+ return $uri->canonical;
+}
+
+sub _common_uri {
+ my $self = shift;
+
+ my $path = $self->env->{SCRIPT_NAME};
+ my $port = $self->env->{SERVER_PORT};
+ my $server = $self->env->{SERVER_NAME};
+ my $host = $self->host;
+ my $scheme = $self->scheme;
+
+ my $uri = URI->new;
+ $uri->scheme($scheme);
+ $uri->authority( $host || "$server:$port" );
+ $uri->path( $path || '/' );
+
+ return $uri;
+}
+
+sub uri_base {
+ my $self = shift;
+ my $uri = $self->_common_uri;
+ my $canon = $uri->canonical;
+
+ if ( $uri->path eq '/' ) {
+ $canon =~ s{/$}{};
+ }
+
+ return $canon;
+}
+
+sub dispatch_path {
+ warn q{request->dispatch_path is deprecated};
+ return shift->path;
+}
+
+sub uri_for {
+ my ( $self, $part, $params, $dont_escape ) = @_;
+
+ $part ||= '';
+ my $uri = $self->base;
+
+ # Make sure there's exactly one slash between the base and the new part
+ my $base = $uri->path;
+ $base =~ s|/$||;
+ $part =~ s|^/||;
+ $uri->path("$base/$part");
+
+ $uri->query_form($params) if $params;
+
+ return $dont_escape
+ ? uri_unescape( ${ $uri->canonical } )
+ : ${ $uri->canonical };
+}
+
+sub params {
+ my ( $self, $source ) = @_;
+
+ return %{ $self->_params } if wantarray && @_ == 1;
+ return $self->_params if @_ == 1;
+
+ if ( $source eq 'query' ) {
+ return %{ $self->_query_params || {} } if wantarray;
+ return $self->_query_params;
+ }
+ elsif ( $source eq 'body' ) {
+ return %{ $self->_body_params || {} } if wantarray;
+ return $self->_body_params;
+ }
+ if ( $source eq 'route' ) {
+ return %{ $self->_route_params } if wantarray;
+ return $self->_route_params;
+ }
+ else {
+ croak "Unknown source params \"$source\".";
+ }
+}
+
+sub query_parameters {
+ my $self = shift;
+ $self->{'query_parameters'} ||= do {
+ if ($XS_PARSE_QUERY_STRING) {
+ my $query = _decode(CGI::Deurl::XS::parse_query_string(
+ $self->env->{'QUERY_STRING'}
+ ));
+
+ Hash::MultiValue->new(
+ map {;
+ my $key = $_;
+ is_arrayref( $query->{$key} )
+ ? ( map +( $key => $_ ), @{ $query->{$key} } )
+ : ( $key => $query->{$key} )
+ } keys %{$query}
+ );
+ } else {
+ # defer to Plack::Request
+ _decode($self->SUPER::query_parameters);
+ }
+ };
+}
+
+# this will be filled once the route is matched
+sub route_parameters { $_[0]->{'route_parameters'} ||= Hash::MultiValue->new }
+
+sub _set_route_parameters {
+ my ( $self, $params ) = @_;
+ # remove reserved splat parameter name
+ # you should access splat parameters using splat() keyword
+ delete @{$params}{qw<splat captures>};
+ $self->{'route_parameters'} = Hash::MultiValue->from_mixed( %{_decode($params)} );
+}
+
+sub body_parameters {
+ my $self = shift;
+ # defer to (the overridden) Plack::Request->body_parameters
+ $self->{'body_parameters'} ||= _decode($self->SUPER::body_parameters());
+}
+
+sub parameters {
+ my ( $self, $type ) = @_;
+
+ # handle a specific case
+ if ($type) {
+ my $attr = "${type}_parameters";
+ return $self->$attr;
+ }
+
+ # merge together the *decoded* parameters
+ $self->{'merged_parameters'} ||= do {
+ my $query = $self->query_parameters;
+ my $body = $self->body_parameters;
+ my $route = $self->route_parameters; # not in Plack::Request
+ Hash::MultiValue->new( map $_->flatten, $query, $body, $route );
+ };
+}
+
+sub captures { shift->params->{captures} || {} }
+
+sub splat { @{ shift->params->{splat} || [] } }
+
+# XXX: incompatible with Plack::Request
+sub param { shift->params->{ $_[0] } }
+
+sub _decode {
+ my ($h) = @_;
+ return if not defined $h;
+
+ if ( !is_ref($h) && !utf8::is_utf8($h) ) {
+ return decode( 'UTF-8', $h );
+ }
+ elsif ( ref($h) eq 'Hash::MultiValue' ) {
+ return Hash::MultiValue->from_mixed(_decode($h->as_hashref_mixed));
+ }
+ elsif ( is_hashref($h) ) {
+ return { map {my $t = _decode($_); $t} (%$h) };
+ }
+ elsif ( is_arrayref($h) ) {
+ return [ map _decode($_), @$h ];
+ }
+
+ return $h;
+}
+
+sub is_ajax {
+ my $self = shift;
+
+ return 0 unless defined $self->headers;
+ return 0 unless defined $self->header('X-Requested-With');
+ return 0 if $self->header('X-Requested-With') ne 'XMLHttpRequest';
+ return 1;
+}
+
+# XXX incompatible with Plack::Request
+# context-aware accessor for uploads
+sub upload {
+ my ( $self, $name ) = @_;
+ my $res = $self->{uploads}{$name};
+
+ return $res unless wantarray;
+ return () unless defined $res;
+ return ( is_arrayref($res) ) ? @$res : $res;
+}
+
+sub _build_params {
+ my ($self) = @_;
+
+ # params may have been populated by before filters
+ # _before_ we get there, so we have to save it first
+ my $previous = $self->_has_params ? $self->_params : {};
+
+ # now parse environment params...
+ my $get_params = $self->_parse_get_params();
+
+ # and merge everything
+ $self->{_params} = {
+ map +( is_hashref($_) ? %{$_} : () ),
+ $previous,
+ $get_params,
+ $self->_body_params,
+ $self->_route_params,
+ };
+
+}
+
+sub _url_decode {
+ my ( $self, $encoded ) = @_;
+ return URL::Encode::XS::url_decode($encoded) if $XS_URL_DECODE;
+ my $clean = $encoded;
+ $clean =~ tr/\+/ /;
+ $clean =~ s/%([a-fA-F0-9]{2})/pack "H2", $1/eg;
+ return $clean;
+}
+
+sub _parse_get_params {
+ my ($self) = @_;
+ return $self->_query_params if defined $self->{_query_params};
+
+ my $query_params = {};
+
+ my $source = $self->env->{QUERY_STRING};
+ return if !defined $source || $source eq '';
+
+ if ($XS_PARSE_QUERY_STRING) {
+ $self->_set_query_params(
+ CGI::Deurl::XS::parse_query_string($source) || {}
+ );
+ return $self->_query_params;
+ }
+
+ foreach my $token ( split /[&;]/, $source ) {
+ my ( $key, $val ) = split( /=/, $token );
+ next unless defined $key;
+ $val = ( defined $val ) ? $val : '';
+ $key = $self->_url_decode($key);
+ $val = $self->_url_decode($val);
+
+ # looking for multi-value params
+ if ( exists $query_params->{$key} ) {
+ my $prev_val = $query_params->{$key};
+ if ( is_arrayref($prev_val) ) {
+ push @{ $query_params->{$key} }, $val;
+ }
+ else {
+ $query_params->{$key} = [ $prev_val, $val ];
+ }
+ }
+
+ # simple value param (first time we see it)
+ else {
+ $query_params->{$key} = $val;
+ }
+ }
+ $self->_set_query_params( $query_params );
+ return $self->_query_params;
+}
+
+sub _build_uploads {
+ my ($self) = @_;
+
+ # parse body and build body params
+ my $body_params = $self->_body_params;
+
+ my $uploads = $self->SUPER::uploads;
+ my %uploads;
+
+ for my $name ( keys %$uploads ) {
+ my @uploads = map Dancer2::Core::Request::Upload->new(
+ # For back-compatibility, we use a HashRef of headers
+ headers => {@{$_->{headers}->psgi_flatten_without_sort}},
+ tempname => $_->{tempname},
+ size => $_->{size},
+ filename => _decode( $_->{filename} ),
+ ), $uploads->get_all($name);
+
+ $uploads{$name} = @uploads > 1 ? \@uploads : $uploads[0];
+
+ # support access to the filename as a normal param
+ my @filenames = map $_->{'filename'}, @uploads;
+ $self->{_body_params}{$name} =
+ @filenames > 1 ? \@filenames : $filenames[0];
+ }
+
+ $self->{uploads} = \%uploads;
+}
+
+# XXX: incompatible with Plack::Request
+sub cookies { $_[0]->{'cookies'} ||= $_[0]->_build_cookies }
+
+sub _build_cookies {
+ my $self = shift;
+ my $cookies = {};
+
+ my $http_cookie = $self->header('Cookie');
+ return $cookies unless defined $http_cookie; # nothing to do
+
+ if ( $XS_HTTP_COOKIES ) {
+ $cookies = HTTP::XSCookies::crush_cookie($http_cookie);
+ }
+ else {
+ # handle via Plack::Request
+ $cookies = $self->SUPER::cookies();
+ }
+
+ # convert to objects
+ while (my ($name, $value) = each %{$cookies}) {
+ $cookies->{$name} = Dancer2::Core::Cookie->new(
+ name => $name,
+ # HTTP::XSCookies v0.17+ will do the split and return an arrayref
+ value => (is_arrayref($value) ? $value : [split(/[&;]/, $value)])
+ );
+ }
+ return $cookies;
+}
+
+# poor man's clone
+sub _shallow_clone {
+ my ($self, $params, $options) = @_;
+
+ # shallow clone $env; we don't want to alter the existing one
+ # in $self, then merge any overridden values
+ my $env = { %{ $self->env }, %{ $options || {} } };
+
+ my $new_request = __PACKAGE__->new(
+ env => $env,
+ body_params => {},
+ );
+
+ # Clone and merge query params
+ my $new_params = $self->params;
+ $new_request->{_query_params} = { %{ $self->{_query_params} || {} } };
+ $new_request->{query_parameters} = $self->query_parameters->clone;
+ for my $key ( keys %{ $params || {} } ) {
+ my $value = $params->{$key};
+ $new_params->{$key} = $value;
+ $new_request->{_query_params}->{$key} = $value;
+ $new_request->{query_parameters}->add( $key => $value );
+ }
+
+ # Copy params (these are already decoded)
+ $new_request->{_params} = $new_params;
+ $new_request->{_body_params} = $self->{_body_params};
+ $new_request->{_route_params} = $self->{_route_params};
+ $new_request->{headers} = $self->headers;
+
+ # Copy remaining settings
+ $new_request->{is_behind_proxy} = $self->{is_behind_proxy};
+ $new_request->{vars} = $self->{vars};
+
+ # Clone any existing decoded & cached body params. (GH#1116 GH#1269)
+ $new_request->{'body_parameters'} = $self->body_parameters->clone;
+
+ # Delete merged HMV parameters, allowing them to be reconstructed on first use.
+ delete $new_request->{'merged_parameters'};
+
+ return $new_request;
+}
+
+
+sub _set_route {
+ my ( $self, $route ) = @_;
+ $self->{'route'} = $route;
+}
+
+sub route { $_[0]->{'route'} }
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Request - Interface for accessing incoming requests
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+In a route handler, the current request object can be accessed by the
+C<request> keyword:
+
+ get '/foo' => sub {
+ request->params; # request, params parsed as a hash ref
+ request->body; # returns the request body, unparsed
+ request->path; # the path requested by the client
+ # ...
+ };
+
+=head1 DESCRIPTION
+
+An object representing a Dancer2 request. It aims to provide a proper
+interface to anything you might need from a web request.
+
+=head1 METHODS
+
+=head2 address
+
+Return the IP address of the client.
+
+=head2 base
+
+Returns an absolute URI for the base of the application. Returns a L<URI>
+object (which stringifies to the URL, as you'd expect).
+
+=head2 body_parameters
+
+Returns a L<Hash::MultiValue> object representing the POST parameters.
+
+=head2 body
+
+Return the raw body of the request, unparsed.
+
+If you need to access the body of the request, you have to use this accessor and
+should not try to read C<psgi.input> by hand. C<Dancer2::Core::Request>
+already did it for you and kept the raw body untouched in there.
+
+=head2 content
+
+Returns the undecoded byte string POST body.
+
+=head2 cookies
+
+Returns a reference to a hash containing cookies, where the keys are the names of the
+cookies and values are L<Dancer2::Core::Cookie> objects.
+
+=head2 data
+
+If the application has a serializer and if the request has serialized
+content, returns the deserialized structure as a hashref.
+
+=head2 dispatch_path
+
+Alias for L<path>. Deprecated.
+
+=head2 env
+
+Return the current PSGI environment hash reference.
+
+=head2 header($name)
+
+Return the value of the given header, if present. If the header has multiple
+values, returns an the list of values if called in list context, the first one
+in scalar.
+
+=head2 headers
+
+Returns either an L<HTTP::Headers> or an L<HTTP::Headers::Fast> object
+representing the headers.
+
+=head2 id
+
+The ID of the request. This allows you to trace a specific request in loggers,
+per the string created using C<to_string>.
+
+The ID of the request is essentially the number of requests run in the current
+class.
+
+=head2 input
+
+Alias to C<input_handle> method below.
+
+=head2 input_handle
+
+Alias to the PSGI input handle (C<< <request->env->{psgi.input}> >>)
+
+=head2 is_ajax
+
+Return true if the value of the header C<X-Requested-With> is
+C<XMLHttpRequest>.
+
+=head2 is_delete
+
+Return true if the method requested by the client is 'DELETE'
+
+=head2 is_get
+
+Return true if the method requested by the client is 'GET'
+
+=head2 is_head
+
+Return true if the method requested by the client is 'HEAD'
+
+=head2 is_post
+
+Return true if the method requested by the client is 'POST'
+
+=head2 is_put
+
+Return true if the method requested by the client is 'PUT'
+
+=head2 is_options
+
+Return true if the method requested by the client is 'OPTIONS'
+
+=head2 logger
+
+Returns the C<psgix.logger> code reference, if exists.
+
+=head2 method
+
+Return the HTTP method used by the client to access the application.
+
+While this method returns the method string as provided by the environment, it's
+better to use one of the following boolean accessors if you want to inspect the
+requested method.
+
+=head2 new
+
+The constructor of the class, used internally by Dancer2's core to create request
+objects.
+
+It uses the environment hash table given to build the request object:
+
+ Dancer2::Core::Request->new( env => $env );
+
+There are two additional parameters for instantiation:
+
+=over 4
+
+=item * serializer
+
+A serializer object to work with when reading the request body.
+
+=item * body_params
+
+Provide body parameters.
+
+Used internally when we need to avoid parsing the body again.
+
+=back
+
+=head2 param($key)
+
+Calls the C<params> method below and fetches the key provided.
+
+=head2 params($source)
+
+Called in scalar context, returns a hashref of params, either from the specified
+source (see below for more info on that) or merging all sources.
+
+So, you can use, for instance:
+
+ my $foo = params->{foo}
+
+If called in list context, returns a list of key and value pairs, so you could use:
+
+ my %allparams = params;
+
+Parameters are merged in the following order: query, body, route - i.e. route
+parameters have the highest priority:
+
+ POST /hello/Ruth?name=Quentin
+
+ name=Bobbie
+
+ post '/hello/:name' => sub {
+ return "Hello, " . route_parameters->get('name') . "!"; # returns Ruth
+ return "Hello, " . query_parameters->get('name') . "!"; # returns Quentin
+ return "Hello, " . body_parameters->get('name') . "!"; # returns Bobbie
+ return "Hello, " . param('name') . "!"; # returns Ruth
+ };
+
+The L</query_parameters>, L</route_parameters>, and L</body_parameters> keywords
+provide a L<Hash::MultiValue> result from the three different parameters.
+We recommend using these rather than C<params>, because of the potential for
+unintentional behaviour - consider the following request and route handler:
+
+ POST /artist/104/new-song
+
+ name=Careless Dancing
+
+ post '/artist/:id/new-song' => sub {
+ find_artist(param('id'))->create_song(params);
+ # oops! we just passed id into create_song,
+ # but we probably only intended to pass name
+ find_artist(param('id'))->create_song(body_parameters);
+ };
+
+ POST /artist/104/join-band
+
+ id=4
+ name=Dancing Misfits
+
+ post '/artist/:id/new-song' => sub {
+ find_artist(param('id'))->join_band(params);
+ # oops! we just passed an id of 104 into join_band,
+ # but we probably should have passed an id of 4
+ };
+
+=head2 parameters
+
+Returns a L<Hash::MultiValue> object with merged GET and POST parameters.
+
+Parameters are merged in the following order: query, body, route - i.e. route
+parameters have the highest priority - see L</params> for how this works, and
+associated risks and alternatives.
+
+=head2 path
+
+The path requested by the client, normalized. This is effectively
+C<path_info> or a single forward C</>.
+
+=head2 path_info
+
+The raw requested path. This could be empty. Use C<path> instead.
+
+=head2 port
+
+Return the port of the server.
+
+=head2 protocol
+
+Return the protocol (I<HTTP/1.0> or I<HTTP/1.1>) used for the request.
+
+=head2 query_parameters
+
+Returns a L<Hash::MultiValue> parameters object.
+
+=head2 query_string
+
+Returns the portion of the request defining the query itself - this is
+what comes after the C<?> in a URI.
+
+=head2 raw_body
+
+Alias to C<content> method.
+
+=head2 remote_address
+
+Alias for C<address> method.
+
+=head2 remote_host
+
+Return the remote host of the client. This only works with web servers configured
+to do a reverse DNS lookup on the client's IP address.
+
+=head2 request_method
+
+Alias to the C<method> accessor, for backward-compatibility with C<CGI> interface.
+
+=head2 request_uri
+
+Return the raw, undecoded request URI path.
+
+=head2 route
+
+Return the L<route|Dancer2::Core::Route> which this request matched.
+
+=head2 scheme
+
+Return the scheme of the request
+
+=head2 script_name
+
+Return script_name from the environment.
+
+=head2 secure
+
+Return true or false, indicating whether the connection is secure - this is
+effectively checking if the scheme is I<HTTPS> or not.
+
+=head2 serializer
+
+Returns the optional serializer object used to deserialize request parameters.
+
+=head2 session
+
+Returns the C<psgix.session> hash, if exists.
+
+=head2 session_options
+
+Returns the C<psgix.session.options> hash, if exists.
+
+=head2 to_string
+
+Return a string representing the request object (e.g., C<GET /some/path>).
+
+=head2 upload($name)
+
+Context-aware accessor for uploads. It's a wrapper around an access to the hash
+table provided by C<uploads()>. It looks at the calling context and returns a
+corresponding value.
+
+If you have many file uploads under the same name, and call C<upload('name')> in
+an array context, the accessor will unroll the ARRAY ref for you:
+
+ my @uploads = request->upload('many_uploads'); # OK
+
+Whereas with a manual access to the hash table, you'll end up with one element
+in C<@uploads>, being the arrayref:
+
+ my @uploads = request->uploads->{'many_uploads'};
+ # $uploads[0]: ARRAY(0xXXXXX)
+
+That is why this accessor should be used instead of a manual access to
+C<uploads>.
+
+=head2 uploads
+
+Returns a reference to a hash containing uploads. Values can be either a
+L<Dancer2::Core::Request::Upload> object, or an arrayref of
+L<Dancer2::Core::Request::Upload>
+objects.
+
+You should probably use the C<upload($name)> accessor instead of manually accessing the
+C<uploads> hash table.
+
+=head2 uri
+
+An alias to C<request_uri>.
+
+=head2 uri_base
+
+Same thing as C<base> above, except it removes the last trailing slash in the
+path if it is the only path.
+
+This means that if your base is I<http://myserver/>, C<uri_base> will return
+I<http://myserver> (notice no trailing slash). This is considered very useful
+when using templates to do the following thing:
+
+ <link rel="stylesheet" href="[% request.uri_base %]/css/style.css" />
+
+=head2 uri_for(path, params)
+
+Constructs a URI from the base and the passed path. If params (hashref) is
+supplied, these are added to the query string of the URI.
+
+Thus, with the following base:
+
+ http://localhost:5000/foo
+
+You get the following behavior:
+
+ my $uri = request->uri_for('/bar', { baz => 'baz' });
+ print $uri; # http://localhost:5000/foo/bar?baz=baz
+
+C<uri_for> returns a L<URI> object (which can stringify to the value).
+
+=head2 user
+
+Return remote user if defined.
+
+=head2 var
+
+By-name interface to variables stored in this request object.
+
+ my $stored = $request->var('some_variable');
+
+returns the value of 'some_variable', while
+
+ $request->var('some_variable' => 'value');
+
+will set it.
+
+=head2 vars
+
+Access to the internal hash of variables:
+
+ my $value = $request->vars->{'my_key'};
+
+You want to use C<var> above.
+
+=head1 Common HTTP request headers
+
+Commonly used client-supplied HTTP request headers are available through
+specific accessors:
+
+=over 4
+
+=item C<accept>
+
+HTTP header: C<HTTP_ACCEPT>.
+
+=item C<accept_charset>
+
+HTTP header: C<HTTP_ACCEPT_CHARSET>.
+
+=item C<accept_encoding>
+
+HTTP header: C<HTTP_ACCEPT_ENCODING>.
+
+=item C<accept_language>
+
+HTTP header: C<HTTP_ACCEPT_LANGUAGE>.
+
+=item C<agent>
+
+Alias for C<user_agent>) below.
+
+=item C<connection>
+
+HTTP header: C<HTTP_CONNECTION>.
+
+=item C<content_encoding>
+
+HTTP header: C<HTTP_CONTENT_ENCODING>.
+
+=item C<content_length>
+
+HTTP header: C<HTTP_CONTENT_LENGTH>.
+
+=item C<content_type>
+
+HTTP header: C<HTTP_CONTENT_TYPE>.
+
+=item C<forwarded_for_address>
+
+HTTP header: C<HTTP_X_FORWARDED_FOR>.
+
+=item C<forwarded_host>
+
+HTTP header: C<HTTP_X_FORWARDED_HOST>.
+
+=item C<forwarded_protocol>
+
+One of either C<HTTP_X_FORWARDED_PROTOCOL>, C<HTTP_X_FORWARDED_PROTO>, or
+C<HTTP_FORWARDED_PROTO>.
+
+=item C<host>
+
+Checks whether we are behind a proxy using the C<behind_proxy>
+configuration option, and if so returns the first
+C<HTTP_X_FORWARDED_HOST>, since this is a comma separated list.
+
+If you have not configured that you are behind a proxy, it returns HTTP
+header C<HTTP_HOST>.
+
+=item C<keep_alive>
+
+HTTP header: C<HTTP_KEEP_ALIVE>.
+
+=item C<referer>
+
+HTTP header: C<HTTP_REFERER>.
+
+=item C<user_agent>
+
+HTTP header: C<HTTP_USER_AGENT>.
+
+=item C<x_requested_with>
+
+HTTP header: C<HTTP_X_REQUESTED_WITH>.
+
+=back
+
+=head1 Fetching only params from a given source
+
+If a required source isn't specified, a mixed hashref (or list of key value
+pairs, in list context) will be returned; this will contain params from all
+sources (route, query, body).
+
+In practical terms, this means that if the param C<foo> is passed both on the
+querystring and in a POST body, you can only access one of them.
+
+If you want to see only params from a given source, you can say so by passing
+the C<$source> param to C<params()>:
+
+ my %querystring_params = params('query');
+ my %route_params = params('route');
+ my %post_params = params('body');
+
+If source equals C<route>, then only params parsed from the route pattern
+are returned.
+
+If source equals C<query>, then only params parsed from the query string are
+returned.
+
+If source equals C<body>, then only params sent in the request body will be
+returned.
+
+If another value is given for C<$source>, then an exception is triggered.
+
+=head1 EXTRA SPEED
+
+If L<Dancer2::Core::Request> detects the following modules as installed,
+it will use them to speed things up:
+
+=over 4
+
+=item * L<URL::Encode::XS>
+
+=item * L<CGI::Deurl::XS>
+
+=back
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Request/Upload.pm b/lib/Dancer2/Core/Request/Upload.pm
new file mode 100644
index 00000000..43b9ab48
--- /dev/null
+++ b/lib/Dancer2/Core/Request/Upload.pm
@@ -0,0 +1,178 @@
+package Dancer2::Core::Request::Upload;
+# ABSTRACT: Class representing file upload requests
+$Dancer2::Core::Request::Upload::VERSION = '0.300000';
+use Moo;
+
+use Carp;
+use File::Spec;
+use Module::Runtime 'require_module';
+
+use Dancer2::Core::Types;
+use Dancer2::FileUtils qw(open_file);
+
+has filename => (
+ is => 'ro',
+ isa => Str,
+);
+
+has tempname => (
+ is => 'ro',
+ isa => Str,
+);
+
+has headers => (
+ is => 'ro',
+ isa => HashRef,
+);
+
+has size => (
+ is => 'ro',
+ isa => Num,
+);
+
+sub file_handle {
+ my ($self) = @_;
+ return $self->{_fh} if defined $self->{_fh};
+ my $fh = open_file( '<', $self->tempname );
+ $self->{_fh} = $fh;
+}
+
+sub copy_to {
+ my ( $self, $target ) = @_;
+ require_module('File::Copy');
+ File::Copy::copy( $self->tempname, $target );
+}
+
+sub link_to {
+ my ( $self, $target ) = @_;
+ CORE::link( $self->tempname, $target );
+}
+
+sub content {
+ my ( $self, $layer ) = @_;
+ return $self->{_content}
+ if defined $self->{_content};
+
+ $layer = ':raw' unless $layer;
+
+ my $content = undef;
+ my $handle = $self->file_handle;
+
+ binmode( $handle, $layer );
+
+ while ( $handle->read( my $buffer, 8192 ) ) {
+ $content .= $buffer;
+ }
+
+ $self->{_content} = $content;
+}
+
+sub basename {
+ my ($self) = @_;
+ require_module('File::Basename');
+ File::Basename::basename( $self->filename );
+}
+
+sub type {
+ my $self = shift;
+ return $self->headers->{'Content-Type'};
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Request::Upload - Class representing file upload requests
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This class implements a representation of file uploads for Dancer2.
+These objects are accessible within route handlers via the request->uploads
+keyword. See L<Dancer2::Core::Request> for details.
+
+=head1 ATTRIBUTES
+
+=head2 filename
+
+Filename as sent by client. optional. May not be undef.
+
+=head2 tempname
+
+The name of the temporary file the data has been saved to. Optional. May not be undef.
+
+=head2 headers
+
+A hash ref of the headers associated with this upload. optional. is read-write and a HashRef.
+
+=head2 size
+
+The size of the upload, in bytes. Optional.
+
+=head1 METHODS
+
+=head2 my $filename=$upload->filename;
+
+Returns the filename (full path) as sent by the client.
+
+=head2 my $tempname=$upload->tempname;
+
+Returns the name of the temporary file the data has been saved to.
+
+For example, in directory /tmp, and given a random name, with no file extension.
+
+=head2 my $href=$upload->headers;
+
+Returns a hashRef of the headers associated with this upload.
+
+=head2 my $fh=$upload->file_handle;
+
+Returns a read-only file handle on the temporary file.
+
+=head2 $upload->copy_to('/path/to/target')
+
+Copies the temporary file using File::Copy. Returns true for success,
+false for failure.
+
+=head2 $upload->link_to('/path/to/target');
+
+Creates a hard link to the temporary file. Returns true for success,
+false for failure.
+
+=head2 my $content=$upload->content;
+
+Returns a scalar containing the contents of the temporary file.
+
+=head2 my $basename=$upload->basename;
+
+Returns basename for "filename".
+
+=head2 $upload->type
+
+Returns the Content-Type of this upload.
+
+=head1 SEE ALSO
+
+L<Dancer2>
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Response.pm b/lib/Dancer2/Core/Response.pm
new file mode 100644
index 00000000..d8be82dd
--- /dev/null
+++ b/lib/Dancer2/Core/Response.pm
@@ -0,0 +1,406 @@
+# ABSTRACT: Response object for Dancer2
+
+package Dancer2::Core::Response;
+$Dancer2::Core::Response::VERSION = '0.300000';
+use Moo;
+
+use Encode;
+use Dancer2::Core::Types;
+
+use Dancer2 ();
+use Dancer2::Core::HTTP;
+
+use HTTP::Headers::Fast;
+use Scalar::Util qw(blessed);
+use Plack::Util;
+use Safe::Isa;
+use Sub::Quote ();
+
+use overload
+ '@{}' => sub { $_[0]->to_psgi },
+ '""' => sub { $_[0] };
+
+has headers => (
+ is => 'ro',
+ isa => InstanceOf['HTTP::Headers'],
+ lazy => 1,
+ coerce => sub {
+ my ($value) = @_;
+ # HTTP::Headers::Fast reports that it isa 'HTTP::Headers',
+ # but there is no actual inheritance.
+ $value->$_isa('HTTP::Headers')
+ ? $value
+ : HTTP::Headers::Fast->new(@{$value});
+ },
+ default => sub {
+ HTTP::Headers::Fast->new();
+ },
+ handles => [qw<header push_header>],
+);
+
+sub headers_to_array {
+ my $self = shift;
+ my $headers = shift || $self->headers;
+
+ my @hdrs;
+ $headers->scan( sub {
+ my ( $k, $v ) = @_;
+ $v =~ s/\015\012[\040|\011]+/chr(32)/ge; # replace LWS with a single SP
+ $v =~ s/\015|\012//g; # remove CR and LF since the char is invalid here
+ push @hdrs, $k => $v;
+ });
+
+ return \@hdrs;
+}
+
+# boolean to tell if the route passes or not
+has has_passed => (
+ is => 'rw',
+ isa => Bool,
+ default => sub {0},
+);
+
+sub pass { shift->has_passed(1) }
+
+has serializer => (
+ is => 'ro',
+ isa => ConsumerOf ['Dancer2::Core::Role::Serializer'],
+);
+
+has is_encoded => (
+ is => 'rw',
+ isa => Bool,
+ default => sub {0},
+);
+
+has is_halted => (
+ is => 'rw',
+ isa => Bool,
+ default => sub {0},
+);
+
+sub halt {
+ my ( $self, $content ) = @_;
+ $self->content( $content ) if @_ > 1;
+ $self->is_halted(1);
+}
+
+has status => (
+ is => 'rw',
+ isa => Num,
+ default => sub {200},
+ lazy => 1,
+ coerce => sub { Dancer2::Core::HTTP->status(shift) },
+);
+
+has content => (
+ is => 'rw',
+ isa => Str,
+ predicate => 'has_content',
+ clearer => 'clear_content',
+);
+
+has server_tokens => (
+ is => 'ro',
+ isa => Bool,
+ default => sub {1},
+);
+
+around content => sub {
+ my ( $orig, $self ) = ( shift, shift );
+
+ # called as getter?
+ @_ or return $self->$orig;
+
+ # No serializer defined; encode content
+ $self->serializer
+ or return $self->$orig( $self->encode_content(@_) );
+
+ # serialize content
+ my $serialized = $self->serialize(@_);
+ $self->is_encoded(1); # All serializers return byte strings
+ return $self->$orig( defined $serialized ? $serialized : '' );
+};
+
+has default_content_type => (
+ is => 'rw',
+ isa => Str,
+ default => sub {'text/html'},
+);
+
+sub encode_content {
+ my ( $self, $content ) = @_;
+
+ return $content if $self->is_encoded;
+
+ # Apply default content type if none set.
+ my $ct = $self->content_type ||
+ $self->content_type( $self->default_content_type );
+
+ return $content if $ct !~ /^text/;
+
+ # we don't want to encode an empty string, it will break the output
+ $content or return $content;
+
+ $self->content_type("$ct; charset=UTF-8")
+ if $ct !~ /charset/;
+
+ $self->is_encoded(1);
+ return Encode::encode( 'UTF-8', $content );
+}
+
+sub new_from_plack {
+ my ($self, $psgi_res) = @_;
+
+ return Dancer2::Core::Response->new(
+ status => $psgi_res->status,
+ headers => $psgi_res->headers,
+ content => $psgi_res->body,
+ );
+}
+
+sub new_from_array {
+ my ($self, $arrayref) = @_;
+
+ return Dancer2::Core::Response->new(
+ status => $arrayref->[0],
+ headers => $arrayref->[1],
+ content => $arrayref->[2][0],
+ );
+}
+
+sub to_psgi {
+ my ($self) = @_;
+
+ $self->server_tokens
+ and $self->header( 'Server' => "Perl Dancer2 " . Dancer2->VERSION );
+
+ my $headers = $self->headers;
+ my $status = $self->status;
+
+ Plack::Util::status_with_no_entity_body($status)
+ and return [ $status, $self->headers_to_array($headers), [] ];
+
+ my $content = $self->content;
+ # It is possible to have no content and/or no content type set
+ # e.g. if all routes 'pass'. Set the default value for the content
+ # (an empty string), allowing serializer hooks to be triggered
+ # as they may change the content..
+ $content = $self->content('') if ! defined $content;
+
+ if ( !$headers->header('Content-Length') &&
+ !$headers->header('Transfer-Encoding') &&
+ defined( my $content_length = length $content ) ) {
+ $headers->push_header( 'Content-Length' => $content_length );
+ }
+
+ # More defaults
+ $self->content_type or $self->content_type($self->default_content_type);
+ return [ $status, $self->headers_to_array($headers), [ $content ], ];
+}
+
+# sugar for accessing the content_type header, with mimetype care
+sub content_type {
+ my $self = shift;
+
+ if ( scalar @_ > 0 ) {
+ my $runner = Dancer2::runner();
+ my $mimetype = $runner->mime_type->name_or_type(shift);
+ $self->header( 'Content-Type' => $mimetype );
+ return $mimetype;
+ }
+ else {
+ return $self->header('Content-Type');
+ }
+}
+
+has _forward => (
+ is => 'rw',
+ isa => HashRef,
+);
+
+sub forward {
+ my ( $self, $uri, $params, $opts ) = @_;
+ $self->_forward( { to_url => $uri, params => $params, options => $opts } );
+}
+
+sub is_forwarded {
+ my $self = shift;
+ $self->_forward;
+}
+
+sub redirect {
+ my ( $self, $destination, $status ) = @_;
+ $self->status( $status || 302 );
+
+ # we want to stringify the $destination object (URI object)
+ $self->header( 'Location' => "$destination" );
+}
+
+sub error {
+ my $self = shift;
+
+ my $error = Dancer2::Core::Error->new(
+ response => $self,
+ @_,
+ );
+
+ $error->throw;
+ return $error;
+}
+
+sub serialize {
+ my ($self, $content) = @_;
+
+ my $serializer = $self->serializer
+ or return;
+
+ $content = $serializer->serialize($content)
+ or return;
+
+ $self->content_type( $serializer->content_type );
+ return $content;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Response - Response object for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 ATTRIBUTES
+
+=head2 is_encoded
+
+Flag to tell if the content has already been encoded.
+
+=head2 is_halted
+
+Flag to tell whether or not the response should continue to be processed.
+
+=head2 status
+
+The HTTP status for the response.
+
+=head2 content
+
+The content for the response, stored as a string. If a reference is passed, the
+response will try coerce it to a string via double quote interpolation.
+
+=head2 default_content_type
+
+Default mime type to use for the response Content-Type header
+if nothing was specified
+
+=head2 headers
+
+The attribute that store the headers in a L<HTTP::Headers::Fast> object.
+
+That attribute coerces from ArrayRef and defaults to an empty L<HTTP::Headers::Fast>
+instance.
+
+=head1 METHODS
+
+=head2 pass
+
+Set has_passed to true.
+
+=head2 serializer()
+
+Returns the optional serializer object used to deserialize request parameters
+
+=head2 halt
+
+Shortcut to halt the current response by setting the is_halted flag.
+
+=head2 encode_content
+
+Encodes the stored content according to the stored L<content_type>. If the content_type
+is a text format C<^text>, then no encoding will take place.
+
+Internally, it uses the L<is_encoded> flag to make sure that content is not encoded twice.
+
+If it encodes the content, then it will return the encoded content. In all other
+cases it returns C<false>.
+
+=head2 new_from_plack
+
+Creates a new response object from a L<Plack::Response> object.
+
+=head2 new_from_array
+
+Creates a new response object from a PSGI arrayref.
+
+=head2 to_psgi
+
+Converts the response object to a PSGI array.
+
+=head2 content_type($type)
+
+A little sugar for setting or accessing the content_type of the response, via the headers.
+
+=head2 redirect ($destination, $status)
+
+Sets a header in this response to give a redirect to $destination, and sets the
+status to $status. If $status is omitted, or false, then it defaults to a status of
+302.
+
+=head2 error( @args )
+
+ $response->error( message => "oops" );
+
+Creates a L<Dancer2::Core::Error> object with the given I<@args> and I<throw()>
+it against the response object. Returns the error object.
+
+=head2 serialize( $content )
+
+ $response->serialize( $content );
+
+Serialize and return $content with the response's serializer.
+set content-type accordingly.
+
+=head2 header($name)
+
+Return the value of the given header, if present. If the header has multiple
+values, returns the list of values if called in list context, the first one
+if in scalar context.
+
+=head2 push_header
+
+Add the header no matter if it already exists or not.
+
+ $self->push_header( 'X-Wing' => '1' );
+
+It can also be called with multiple values to add many times the same header
+with different values:
+
+ $self->push_header( 'X-Wing' => 1, 2, 3 );
+
+=head2 headers_to_array($headers)
+
+Convert the C<$headers> to a PSGI ArrayRef.
+
+If no C<$headers> are provided, it will use the current response headers.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Response/Delayed.pm b/lib/Dancer2/Core/Response/Delayed.pm
new file mode 100644
index 00000000..55dc5bd1
--- /dev/null
+++ b/lib/Dancer2/Core/Response/Delayed.pm
@@ -0,0 +1,175 @@
+package Dancer2::Core::Response::Delayed;
+# ABSTRACT: Delayed responses
+$Dancer2::Core::Response::Delayed::VERSION = '0.300000';
+use Moo;
+use Dancer2::Core::Types qw<CodeRef InstanceOf>;
+
+has request => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::Request'],
+ required => 1,
+);
+
+has response => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::Response'],
+ required => 1,
+ handles => [qw/status headers/],
+);
+
+has cb => (
+ is => 'ro',
+ isa => CodeRef,
+ required => 1,
+);
+
+has error_cb => (
+ is => 'ro',
+ isa => CodeRef,
+ predicate => '_has_error_cb',
+);
+
+sub is_halted() {0}
+sub has_passed() {0}
+
+sub to_psgi {
+ my $self = shift;
+
+ return sub {
+ my $responder = shift;
+
+ local $Dancer2::Core::Route::REQUEST = $self->request;
+ local $Dancer2::Core::Route::RESPONSE = $self->response;
+ local $Dancer2::Core::Route::RESPONDER = $responder;
+ local $Dancer2::Core::Route::WRITER;
+
+ local $Dancer2::Core::Route::ERROR_HANDLER =
+ $self->_has_error_cb ? $self->error_cb : undef;
+
+ $self->cb->();
+ };
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Response::Delayed - Delayed responses
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ my $response = Dancer2::Core::Response::Delayed->new(
+ request => Dancer2::Core::Request->new(...),
+ response => Dancer2::Core::Response->new(...),
+ cb => sub {...},
+
+ # optional error handling
+ error_cb => sub {
+ my ($error) = @_;
+ ...
+ },
+ );
+
+ # or in an app
+ get '/' => sub {
+ # delayed response:
+ delayed {
+ # streaming content
+ content "data";
+ content "more data";
+
+ # close user connection
+ done;
+ } on_error => sub {
+ my ($error) = @_;
+ warning 'Failed to stream to user: ' . request->remote_address;
+ };
+ };
+
+=head1 DESCRIPTION
+
+This object represents a delayed (asynchronous) response for L<Dancer2>.
+It can be used via the C<delayed> keyword.
+
+It keeps references to a request and a response in order to avoid
+keeping a reference to the application.
+
+=head1 ATTRIBUTES
+
+=head2 request
+
+Contains a request the delayed response uses.
+
+In the context of a web request, this will be the request that existed
+when the delayed response has been created.
+
+=head2 response
+
+Contains a response the delayed response uses.
+
+In the context of a web request, this will be the response that existed
+when the delayed response has been created.
+
+=head2 cb
+
+The code that will be run asynchronously.
+
+=head2 error_cb
+
+A callback for handling errors. This callback receives the error as its
+first (and currently only) parameter.
+
+=head1 METHODS
+
+=head2 is_halted
+
+A method indicating whether the response has halted.
+
+This is useless in the context of an asynchronous request so it simply
+returns no.
+
+This method is likely going away.
+
+=head2 has_passed
+
+A method indicating whether the response asked to skip the current
+response.
+
+This is useless in the context of an asynchronous request so it simply
+returns no.
+
+This method is likely going away.
+
+=head2 to_psgi
+
+Create a PSGI response. The way it works is by returning a proper PSGI
+response subroutine which localizes the request and response (in case
+the callback wants to edit them without a reference to them), and then
+calls the callback.
+
+Finally, when the callback is done, it asks the response (whether it
+was changed or not) to create its own PSGI response (calling C<to_psgi>)
+and sends that to the callback it receives as a delayed response.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/ConfigReader.pm b/lib/Dancer2/Core/Role/ConfigReader.pm
new file mode 100644
index 00000000..83d58540
--- /dev/null
+++ b/lib/Dancer2/Core/Role/ConfigReader.pm
@@ -0,0 +1,357 @@
+# ABSTRACT: Config role for Dancer2 core objects
+package Dancer2::Core::Role::ConfigReader;
+$Dancer2::Core::Role::ConfigReader::VERSION = '0.300000';
+use Moo::Role;
+
+use File::Spec;
+use Config::Any;
+use Hash::Merge::Simple;
+use Carp 'croak';
+use Module::Runtime 'require_module';
+
+use Dancer2::Core::Factory;
+use Dancer2::Core;
+use Dancer2::Core::Types;
+use Dancer2::FileUtils 'path';
+
+with 'Dancer2::Core::Role::HasLocation';
+
+has default_config => (
+ is => 'ro',
+ isa => HashRef,
+ lazy => 1,
+ builder => '_build_default_config',
+);
+
+has config_location => (
+ is => 'ro',
+ isa => ReadableFilePath,
+ lazy => 1,
+ default => sub { $ENV{DANCER_CONFDIR} || $_[0]->location },
+);
+
+# The type for this attribute is Str because we don't require
+# an existing directory with configuration files for the
+# environments. An application without environments is still
+# valid and works.
+has environments_location => (
+ is => 'ro',
+ isa => Str,
+ lazy => 1,
+ default => sub {
+ $ENV{DANCER_ENVDIR}
+ || File::Spec->catdir( $_[0]->config_location, 'environments' )
+ || File::Spec->catdir( $_[0]->location, 'environments' );
+ },
+);
+
+has config => (
+ is => 'ro',
+ isa => HashRef,
+ lazy => 1,
+ builder => '_build_config',
+);
+
+has environment => (
+ is => 'ro',
+ isa => Str,
+ lazy => 1,
+ builder => '_build_environment',
+);
+
+has config_files => (
+ is => 'ro',
+ lazy => 1,
+ isa => ArrayRef,
+ builder => '_build_config_files',
+);
+
+has local_triggers => (
+ is => 'ro',
+ isa => HashRef,
+ default => sub { +{} },
+);
+
+has global_triggers => (
+ is => 'ro',
+ isa => HashRef,
+ default => sub {
+ my $triggers = {
+ traces => sub {
+ my ( $self, $traces ) = @_;
+ # Carp is already a dependency
+ $Carp::Verbose = $traces ? 1 : 0;
+ },
+ };
+
+ my $runner_config = defined $Dancer2::runner
+ ? Dancer2->runner->config
+ : {};
+
+ for my $global ( keys %$runner_config ) {
+ next if exists $triggers->{$global};
+ $triggers->{$global} = sub {
+ my ($self, $value) = @_;
+ Dancer2->runner->config->{$global} = $value;
+ }
+ }
+
+ return $triggers;
+ },
+);
+
+sub _build_default_config { +{} }
+
+sub _build_environment { 'development' }
+
+sub _build_config_files {
+ my ($self) = @_;
+
+ my $location = $self->config_location;
+ # an undef location means no config files for the caller
+ return [] unless defined $location;
+
+ my $running_env = $self->environment;
+ my @available_exts = Config::Any->extensions;
+ my @files;
+
+ my @exts = @available_exts;
+ if (my $ext = $ENV{DANCER_CONFIG_EXT}) {
+ if (grep { $ext eq $_ } @available_exts) {
+ @exts = $ext;
+ warn "Only looking for configs ending in '$ext'\n"
+ if $ENV{DANCER_CONFIG_VERBOSE};
+ } else {
+ warn "DANCER_CONFIG_EXT environment variable set to '$ext' which\n" .
+ "is not recognized by Config::Any. Looking for config file\n" .
+ "using default list of extensions:\n" .
+ "\t@available_exts\n";
+ }
+ }
+
+ foreach my $file ( [ $location, "config" ],
+ [ $self->environments_location, $running_env ] )
+ {
+ foreach my $ext (@exts) {
+ my $path = path( $file->[0], $file->[1] . ".$ext" );
+ next if !-r $path;
+
+ # Look for *_local.ext files
+ my $local = path( $file->[0], $file->[1] . "_local.$ext" );
+ push @files, $path, ( -r $local ? $local : () );
+ }
+ }
+
+ return \@files;
+}
+
+sub _build_config {
+ my ($self) = @_;
+
+ my $location = $self->config_location;
+ my $default = $self->default_config;
+
+ my $config = Hash::Merge::Simple->merge(
+ $default,
+ map {
+ warn "Merging config file $_\n" if $ENV{DANCER_CONFIG_VERBOSE};
+ $self->load_config_file($_)
+ } @{ $self->config_files }
+ );
+
+ $config = $self->_normalize_config($config);
+ return $config;
+}
+
+sub _set_config_entries {
+ my ( $self, @args ) = @_;
+ my $no = scalar @args;
+ while (@args) {
+ $self->_set_config_entry( shift(@args), shift(@args) );
+ }
+ return $no;
+}
+
+sub _set_config_entry {
+ my ( $self, $name, $value ) = @_;
+
+ $value = $self->_normalize_config_entry( $name, $value );
+ $value = $self->_compile_config_entry( $name, $value, $self->config );
+ $self->config->{$name} = $value;
+}
+
+sub _normalize_config {
+ my ( $self, $config ) = @_;
+
+ foreach my $key ( keys %{$config} ) {
+ my $value = $config->{$key};
+ $config->{$key} = $self->_normalize_config_entry( $key, $value );
+ }
+ return $config;
+}
+
+sub _compile_config {
+ my ( $self, $config ) = @_;
+
+ foreach my $key ( keys %{$config} ) {
+ my $value = $config->{$key};
+ $config->{$key} =
+ $self->_compile_config_entry( $key, $value, $config );
+ }
+ return $config;
+}
+
+sub settings { shift->config }
+
+sub setting {
+ my $self = shift;
+ my @args = @_;
+
+ return ( scalar @args == 1 )
+ ? $self->settings->{ $args[0] }
+ : $self->_set_config_entries(@args);
+}
+
+sub has_setting {
+ my ( $self, $name ) = @_;
+ return exists $self->config->{$name};
+}
+
+sub load_config_file {
+ my ( $self, $file ) = @_;
+ my $config;
+
+ eval {
+ my @files = ($file);
+ my $tmpconfig =
+ Config::Any->load_files( { files => \@files, use_ext => 1 } )->[0];
+ ( $file, $config ) = %{$tmpconfig} if defined $tmpconfig;
+ };
+ if ( my $err = $@ || ( !$config ) ) {
+ croak "Unable to parse the configuration file: $file: $@";
+ }
+
+ # TODO handle mergeable entries
+ return $config;
+}
+
+# private
+
+my $_normalizers = {
+ charset => sub {
+ my ($charset) = @_;
+ return $charset if !length( $charset || '' );
+
+ require_module('Encode');
+ my $encoding = Encode::find_encoding($charset);
+ croak
+ "Charset defined in configuration is wrong : couldn't identify '$charset'"
+ unless defined $encoding;
+ my $name = $encoding->name;
+
+ # Perl makes a distinction between the usual perl utf8, and the strict
+ # utf8 charset. But we don't want to make this distinction
+ $name = 'utf-8' if $name eq 'utf-8-strict';
+ return $name;
+ },
+};
+
+sub _normalize_config_entry {
+ my ( $self, $name, $value ) = @_;
+ $value = $_normalizers->{$name}->($value)
+ if exists $_normalizers->{$name};
+ return $value;
+}
+
+sub _compile_config_entry {
+ my ( $self, $name, $value, $config ) = @_;
+
+ my $trigger = exists $self->local_triggers->{$name} ?
+ $self->local_triggers->{$name} :
+ $self->global_triggers->{$name};
+
+ defined $trigger or return $value;
+
+ return $trigger->( $self, $value, $config );
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::ConfigReader - Config role for Dancer2 core objects
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Provides a C<config> attribute that feeds itself by finding and parsing
+configuration files.
+
+Also provides a C<setting()> method which is supposed to be used by externals to
+read/write config entries.
+
+=head1 ATTRIBUTES
+
+=head2 location
+
+Absolute path to the directory where the server started.
+
+=head2 config_location
+
+Gets the location from the configuration. Same as C<< $object->location >>.
+
+=head2 environments_location
+
+Gets the directory were the environment files are stored.
+
+=head2 config
+
+Returns the whole configuration.
+
+=head2 environments
+
+Returns the name of the environment.
+
+=head2 config_files
+
+List of all the configuration files.
+
+=head1 METHODS
+
+=head2 settings
+
+Alias for config. Equivalent to <<$object->config>>.
+
+=head2 setting
+
+Get or set an element from the configuration.
+
+=head2 has_setting
+
+Verifies that a key exists in the configuration.
+
+=head2 load_config_file
+
+Load the configuration files.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/DSL.pm b/lib/Dancer2/Core/Role/DSL.pm
new file mode 100644
index 00000000..fc6de500
--- /dev/null
+++ b/lib/Dancer2/Core/Role/DSL.pm
@@ -0,0 +1,130 @@
+package Dancer2::Core::Role::DSL;
+# ABSTRACT: Role for DSL
+$Dancer2::Core::Role::DSL::VERSION = '0.300000';
+use Moo::Role;
+use Dancer2::Core::Types;
+use Carp 'croak';
+use Scalar::Util qw();
+
+with 'Dancer2::Core::Role::Hookable';
+
+has app => ( is => 'ro', required => 1 );
+
+has keywords => (
+ is => 'rw',
+ isa => HashRef,
+ lazy => 1,
+ builder => '_build_dsl_keywords',
+);
+
+sub _build_dsl_keywords {
+ my ($self) = @_;
+ $self->can('dsl_keywords')
+ ? $self->dsl_keywords
+ : {};
+}
+
+sub register {
+ my ( $self, $keyword, $is_global ) = @_;
+ my $keywords = $self->keywords;
+ my $pkg = ref($self);
+ $pkg =~ s/__WITH__.+$//;
+
+ if ( exists $keywords->{$keyword} ) {
+ my $reg_pkg = $keywords->{$keyword}{'pkg'};
+ $reg_pkg =~ s/__WITH__.+$//;
+ $reg_pkg eq $pkg and return;
+
+ croak "[$pkg] Keyword $keyword already registered by $reg_pkg";
+ }
+
+ $keywords->{$keyword} = { is_global => $is_global, pkg => $pkg };
+}
+
+sub dsl { $_[0] }
+
+# exports new symbol to caller
+sub export_symbols_to {
+ my ( $self, $caller, $args ) = @_;
+ my $exports = $self->_construct_export_map($args);
+
+ ## no critic
+ foreach my $export ( keys %{$exports} ) {
+ no strict 'refs'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
+ my $existing = *{"${caller}::${export}"}{CODE};
+
+ next if defined $existing;
+
+ *{"${caller}::${export}"} = $exports->{$export};
+ }
+ ## use critic
+
+ return keys %{$exports};
+}
+
+# private
+
+sub _compile_keyword {
+ my ( $self, $keyword, $opts ) = @_;
+
+ my $code = $opts->{is_global}
+ ? sub { $self->$keyword(@_) }
+ : sub {
+ croak "Function '$keyword' must be called from a route handler"
+ unless defined $Dancer2::Core::Route::REQUEST;
+
+ $self->$keyword(@_)
+ };
+
+ return $self->_apply_prototype($code, $opts);
+}
+
+sub _apply_prototype {
+ my ($self, $code, $opts) = @_;
+
+ # set prototype if one is defined for the keyword. undef => no prototype
+ my $prototype;
+ exists $opts->{'prototype'} and $prototype = $opts->{'prototype'};
+ return Scalar::Util::set_prototype( \&$code, $prototype );
+}
+
+sub _construct_export_map {
+ my ( $self, $args ) = @_;
+ my $keywords = $self->keywords;
+ my %map;
+ foreach my $keyword ( keys %$keywords ) {
+ # check if the keyword were excluded from importation
+ $args->{ '!' . $keyword } and next;
+ $map{$keyword} = $self->_compile_keyword( $keyword, $keywords->{$keyword} );
+ }
+ return \%map;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::DSL - Role for DSL
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/Engine.pm b/lib/Dancer2/Core/Role/Engine.pm
new file mode 100644
index 00000000..affd66de
--- /dev/null
+++ b/lib/Dancer2/Core/Role/Engine.pm
@@ -0,0 +1,71 @@
+package Dancer2::Core::Role::Engine;
+# ABSTRACT: Role for engines
+$Dancer2::Core::Role::Engine::VERSION = '0.300000';
+use Moo::Role;
+use Dancer2::Core::Types;
+
+with 'Dancer2::Core::Role::Hookable';
+
+has session => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::Session'],
+ writer => 'set_session',
+ clearer => 'clear_session',
+ predicate => 'has_session',
+);
+
+has config => (
+ is => 'ro',
+ isa => HashRef,
+ default => sub { {} },
+);
+
+has request => (
+ is => 'ro',
+ isa => InstanceOf['Dancer2::Core::Request'],
+ writer => 'set_request',
+ clearer => 'clear_request',
+ predicate => 'has_request',
+);
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::Engine - Role for engines
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This role is intended to be consumed by all engine roles. It contains all the
+shared logic for engines.
+
+This role consumes the L<Dancer2::Core::Role::Hookable> role.
+
+=head1 ATTRIBUTES
+
+=head2 config
+
+An HashRef that hosts the configuration bits for the engine.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/Handler.pm b/lib/Dancer2/Core/Role/Handler.pm
new file mode 100644
index 00000000..1e55e92d
--- /dev/null
+++ b/lib/Dancer2/Core/Role/Handler.pm
@@ -0,0 +1,52 @@
+package Dancer2::Core::Role::Handler;
+# ABSTRACT: Role for Handlers
+$Dancer2::Core::Role::Handler::VERSION = '0.300000';
+use Moo::Role;
+use Dancer2::Core::Types;
+
+requires 'register';
+
+has app => (
+ is => 'ro',
+ isa => InstanceOf ['Dancer2::Core::App'],
+ weak_ref => 1,
+);
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::Handler - Role for Handlers
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 ATTRIBUTES
+
+=head2 app
+
+Contain an object of class L<Dancer2::Core::App>.
+
+=head1 REQUIREMENTS
+
+This role requires the method C<register> to be implemented.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/HasLocation.pm b/lib/Dancer2/Core/Role/HasLocation.pm
new file mode 100644
index 00000000..facb5c91
--- /dev/null
+++ b/lib/Dancer2/Core/Role/HasLocation.pm
@@ -0,0 +1,104 @@
+package Dancer2::Core::Role::HasLocation;
+# ABSTRACT: Role for application location "guessing"
+$Dancer2::Core::Role::HasLocation::VERSION = '0.300000';
+use Moo::Role;
+use Dancer2::Core::Types;
+use Dancer2::FileUtils;
+use File::Spec;
+use Sub::Quote 'quote_sub';
+
+# the path to the caller script/app
+# Note: to remove any ambiguity between the accessor for the
+# 'caller' attribute and the core function caller(), explicitly
+# specify we want the function 'CORE::caller' as the default for
+# the attribute.
+has caller => (
+ is => 'ro',
+ isa => Str,
+ default => quote_sub( q{
+ my ( $caller, $script ) = CORE::caller;
+ $script = File::Spec->abs2rel( $script ) if File::Spec->file_name_is_absolute( $script );
+ $script;
+ } ),
+);
+
+has location => (
+ is => 'ro',
+ builder => '_build_location',
+);
+
+# FIXME: i hate you most of all -- Sawyer X
+sub _build_location {
+ my $self = shift;
+ my $script = $self->caller;
+
+ # default to the dir that contains the script...
+ my $location = Dancer2::FileUtils::dirname($script);
+
+ #we try to find bin and lib
+ my $subdir = $location;
+ my $subdir_found = 0;
+
+ #maximum of 10 iterations, to prevent infinite loop
+ for ( 1 .. 10 ) {
+
+ #try to find libdir and bindir to determine the root of dancer app
+ my $libdir = Dancer2::FileUtils::path( $subdir, 'lib' );
+ my $bindir = Dancer2::FileUtils::path( $subdir, 'bin' );
+
+ #try to find .dancer_app file to determine the root of dancer app
+ my $dancerdir = Dancer2::FileUtils::path( $subdir, '.dancer' );
+
+ # if one of them is found, keep that; but skip ./blib since both lib and bin exist
+ # under it, but views and public do not.
+ if (
+ ( $subdir !~ m![\\/]blib[\\/]?$! && -d $libdir && -d $bindir ) ||
+ ( -f $dancerdir )
+ ) {
+ $subdir_found = 1;
+ last;
+ }
+
+ $subdir = Dancer2::FileUtils::path( $subdir, '..' ) || '.';
+ last if File::Spec->rel2abs($subdir) eq File::Spec->rootdir;
+
+ }
+
+ my $path = $subdir_found ? $subdir : $location;
+
+ # return if absolute
+ File::Spec->file_name_is_absolute($path)
+ and return $path;
+
+ # convert relative to absolute
+ return File::Spec->rel2abs($path);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::HasLocation - Role for application location "guessing"
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/Hookable.pm b/lib/Dancer2/Core/Role/Hookable.pm
new file mode 100644
index 00000000..cfe4bade
--- /dev/null
+++ b/lib/Dancer2/Core/Role/Hookable.pm
@@ -0,0 +1,146 @@
+package Dancer2::Core::Role::Hookable;
+# ABSTRACT: Role for hookable objects
+$Dancer2::Core::Role::Hookable::VERSION = '0.300000';
+use Moo::Role;
+use Dancer2::Core;
+use Dancer2::Core::Types;
+use Carp 'croak';
+use Safe::Isa;
+
+requires 'supported_hooks', 'hook_aliases';
+
+# The hooks registry
+has hooks => (
+ is => 'ro',
+ isa => HashRef,
+ builder => '_build_hooks',
+ lazy => 1,
+);
+
+sub BUILD { }
+
+# after a hookable object is built, we go over its postponed hooks and register
+# them if any.
+after BUILD => sub {
+ my ( $self, $args ) = @_;
+ $self->_add_postponed_hooks($args)
+ if defined $args->{postponed_hooks};
+};
+
+sub _add_postponed_hooks {
+ my ( $self, $args ) = @_;
+ my $postponed_hooks = $args->{postponed_hooks};
+
+ # find the internal name of the hooks, from the caller name
+ my $caller = ref($self);
+ my ( $dancer, $h_type, $h_name, @rest ) = map lc, split /::/, $caller;
+ $h_name = $rest[0] if $h_name eq 'role';
+ if ( $h_type =~ /(template|logger|serializer|session)/ ) {
+ $h_name = $h_type;
+ $h_type = 'engine';
+ }
+
+ # keep only the hooks we want
+ $postponed_hooks = $postponed_hooks->{$h_type}{$h_name};
+ return unless defined $postponed_hooks;
+
+ foreach my $name ( keys %{$postponed_hooks} ) {
+ my $hook = $postponed_hooks->{$name}{hook};
+ my $caller = $postponed_hooks->{$name}{caller};
+
+ $self->has_hook($name)
+ or croak "$h_name $h_type does not support the hook `$name'. ("
+ . join( ", ", @{$caller} ) . ")";
+
+ $self->add_hook($hook);
+ }
+}
+
+# mst++ for the hint
+sub _build_hooks {
+ my ($self) = @_;
+ my %hooks = map +( $_ => [] ), $self->supported_hooks;
+ return \%hooks;
+}
+
+# This binds a coderef to an installed hook if not already
+# existing
+sub add_hook {
+ my ( $self, $hook ) = @_;
+ my $name = $hook->name;
+ my $code = $hook->code;
+
+ croak "Unsupported hook '$name'"
+ unless $self->has_hook($name);
+
+ push @{ $self->hooks->{$name} }, $code;
+}
+
+# allows the caller to replace the current list of hooks at the given position
+# this is useful if the object where this role is composed wants to compile the
+# hooks.
+sub replace_hook {
+ my ( $self, $position, $hooks ) = @_;
+
+ croak "Hook '$position' must be installed first"
+ unless $self->has_hook($position);
+
+ $self->hooks->{$position} = $hooks;
+}
+
+# Boolean flag to tells if the hook is registered or not
+sub has_hook {
+ my ( $self, $hook_name ) = @_;
+ return exists $self->hooks->{$hook_name};
+}
+
+# Execute the hook at the given position
+sub execute_hook {
+ my $self = shift;
+ my $name = shift;
+
+ $name and !ref $name
+ or croak "execute_hook needs a hook name";
+
+ $name = $self->hook_aliases->{$name}
+ if exists $self->hook_aliases->{$name};
+
+ croak "Hook '$name' does not exist"
+ if !$self->has_hook($name);
+
+ $self->$_isa('Dancer2::Core::App') &&
+ $self->log( core => "Entering hook $name" );
+
+ for my $hook ( @{ $self->hooks->{$name} } ) {
+ $hook->(@_);
+ }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::Hookable - Role for hookable objects
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/Logger.pm b/lib/Dancer2/Core/Role/Logger.pm
new file mode 100644
index 00000000..25b81266
--- /dev/null
+++ b/lib/Dancer2/Core/Role/Logger.pm
@@ -0,0 +1,345 @@
+package Dancer2::Core::Role::Logger;
+# ABSTRACT: Role for logger engines
+$Dancer2::Core::Role::Logger::VERSION = '0.300000';
+use Dancer2::Core::Types;
+
+use Moo::Role;
+use POSIX 'strftime';
+use Encode ();
+use Data::Dumper;
+
+with 'Dancer2::Core::Role::Engine';
+
+sub hook_aliases { +{} }
+sub supported_hooks {
+ qw(
+ engine.logger.before
+ engine.logger.after
+ );
+}
+
+sub _build_type {'Logger'}
+
+# This is the only method to implement by logger engines.
+# It receives the following arguments:
+# $msg_level, $msg_content, it gets called only if the configuration allows
+# a message of the given level to be logged.
+requires 'log';
+
+has auto_encoding_charset => (
+ is => 'ro',
+ isa => Str,
+);
+
+has app_name => (
+ is => 'ro',
+ isa => Str,
+ default => sub {'-'},
+);
+
+has log_format => (
+ is => 'rw',
+ isa => Str,
+ default => sub {'[%a:%P] %L @%T> %m in %f l. %l'},
+);
+
+my $_levels = {
+
+ # levels < 0 are for core only
+ core => -10,
+
+ # levels > 0 are for end-users only
+ debug => 1,
+ info => 2,
+ warn => 3,
+ warning => 3,
+ error => 4,
+};
+
+has log_level => (
+ is => 'rw',
+ isa => Enum[keys %{$_levels}],
+ default => sub {'debug'},
+);
+
+sub _should {
+ my ( $self, $msg_level ) = @_;
+ my $conf_level = $self->log_level;
+ return $_levels->{$conf_level} <= $_levels->{$msg_level};
+}
+
+sub format_message {
+ my ( $self, $level, $message ) = @_;
+ chomp $message;
+
+ $message = Encode::encode( $self->auto_encoding_charset, $message )
+ if $self->auto_encoding_charset;
+
+ my @stack = caller(8);
+ my $request = $self->request;
+ my $config = $self->config;
+
+ my $block_handler = sub {
+ my ( $block, $type ) = @_;
+ if ( $type eq 't' ) {
+ return Encode::decode(
+ $config->{'charset'} || 'UTF-8',
+ POSIX::strftime( $block, localtime(time) )
+ );
+ }
+ elsif ( $type eq 'h' ) {
+ return ( $request && $request->header($block) ) || '-';
+ }
+ else {
+ Carp::carp("{$block}$type not supported");
+ return "-";
+ }
+ };
+
+ my $chars_mapping = {
+ a => sub { $self->app_name },
+ t => sub {
+ Encode::decode(
+ $config->{'charset'} || 'UTF-8',
+ POSIX::strftime( "%d/%b/%Y %H:%M:%S", localtime(time) )
+ );
+ },
+ T => sub { POSIX::strftime( "%Y-%m-%d %H:%M:%S", localtime(time) ) },
+ u => sub {
+ Encode::decode(
+ $config->{'charset'} || 'UTF-8',
+ POSIX::strftime( "%d/%b/%Y %H:%M:%S", gmtime(time) )
+ );
+ },
+ U => sub { POSIX::strftime( "%Y-%m-%d %H:%M:%S", gmtime(time) ) },
+ P => sub {$$},
+ L => sub {$level},
+ m => sub {$message},
+ f => sub { $stack[1] || '-' },
+ l => sub { $stack[2] || '-' },
+ h => sub {
+ ( $request && ( $request->remote_host || $request->address ) ) || '-'
+ },
+ i => sub { ( $request && $request->id ) || '-' },
+ };
+
+ my $char_mapping = sub {
+ my $char = shift;
+
+ my $cb = $chars_mapping->{$char};
+ if ( !$cb ) {
+ Carp::carp "%$char not supported.";
+ return "-";
+ }
+ $cb->($char);
+ };
+
+ my $fmt = $self->log_format;
+
+ $fmt =~ s/
+ (?:
+ \%\{(.+?)\}([a-z])|
+ \%([a-zA-Z])
+ )
+ / $1 ? $block_handler->($1, $2) : $char_mapping->($3) /egx;
+
+ return $fmt . "\n";
+}
+
+sub _serialize {
+ my @vars = @_;
+
+ return join q{}, map +(
+ ref $_
+ ? Data::Dumper->new( [$_] )->Terse(1)->Purity(1)->Indent(0)
+ ->Sortkeys(1)->Dump()
+ : ( defined($_) ? $_ : 'undef' )
+ ), @vars;
+}
+
+around 'log' => sub {
+ my ($orig, $self, @args) = @_;
+
+ $self->execute_hook( 'engine.logger.before', $self, @args );
+ $self->$orig( @args );
+ $self->execute_hook( 'engine.logger.after', $self, @args );
+};
+
+sub core {
+ my ( $self, @args ) = @_;
+ $self->_should('core') and $self->log( 'core', _serialize(@args) );
+}
+
+sub debug {
+ my ( $self, @args ) = @_;
+ $self->_should('debug') and $self->log( 'debug', _serialize(@args) );
+}
+
+sub info {
+ my ( $self, @args ) = @_;
+ $self->_should('info') and $self->log( 'info', _serialize(@args) );
+}
+
+sub warning {
+ my ( $self, @args ) = @_;
+ $self->_should('warning') and $self->log( 'warning', _serialize(@args) );
+}
+
+sub error {
+ my ( $self, @args ) = @_;
+ $self->_should('error') and $self->log( 'error', _serialize(@args) );
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::Logger - Role for logger engines
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Any class that consumes this role will be able to implement to write log messages.
+
+In order to implement this role, the consumer B<must> implement the C<log>
+method. This method will receives as argument the C<level> and the C<message>.
+
+=head1 ATTRIBUTES
+
+=head2 auto_encoding_charset
+
+Charset to use when writing a message.
+
+=head2 app_name
+
+Name of the application. Can be used in the message.
+
+=head2 log_format
+
+This is a format string (or a preset name) to specify the log format.
+
+The possible values are:
+
+=over 4
+
+=item %h
+
+host emitting the request
+
+=item %t
+
+date (local timezone, formatted like %d/%b/%Y %H:%M:%S)
+
+=item %T
+
+date (local timezone, formatted like %Y-%m-%d %H:%M:%S)
+
+=item %u
+
+date (UTC timezone, formatted like %d/%b/%Y %H:%M:%S)
+
+=item %U
+
+date (UTC timezone, formatted like %Y-%m-%d %H:%M:%S)
+
+=item %P
+
+PID
+
+=item %L
+
+log level
+
+=item %D
+
+timer
+
+=item %m
+
+message
+
+=item %f
+
+file name that emit the message
+
+=item %l
+
+line from the file
+
+=item %i
+
+request ID
+
+=item %{$fmt}t
+
+timer formatted with a valid time format
+
+=item %{header}h
+
+header value
+
+=back
+
+=head2 log_level
+
+Level to use by default.
+
+=head1 METHODS
+
+=head2 core
+
+Log messages as B<core>.
+
+=head2 debug
+
+Log messages as B<debug>.
+
+=head2 info
+
+Log messages as B<info>.
+
+=head2 warning
+
+Log messages as B<warning>.
+
+=head2 error
+
+Log messages as B<error>.
+
+=head2 format_message
+
+Provides a common message formatting.
+
+=head1 CONFIGURATION
+
+The B<logger> configuration variable tells Dancer2 which engine to use.
+
+You can change it either in your config.yml file:
+
+ # logging to console
+ logger: "console"
+
+The log format can also be configured,
+please see L<Dancer2::Core::Role::Logger/"log_format"> for details.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/Serializer.pm b/lib/Dancer2/Core/Role/Serializer.pm
new file mode 100644
index 00000000..49e78300
--- /dev/null
+++ b/lib/Dancer2/Core/Role/Serializer.pm
@@ -0,0 +1,165 @@
+package Dancer2::Core::Role::Serializer;
+# ABSTRACT: Role for Serializer engines
+$Dancer2::Core::Role::Serializer::VERSION = '0.300000';
+use Moo::Role;
+use Dancer2::Core::Types;
+use Scalar::Util 'blessed';
+
+with 'Dancer2::Core::Role::Engine';
+
+sub hook_aliases {
+ {
+ before_serializer => 'engine.serializer.before',
+ after_serializer => 'engine.serializer.after',
+ }
+}
+
+sub supported_hooks { values %{ shift->hook_aliases } }
+
+sub _build_type {'Serializer'}
+
+requires 'serialize';
+requires 'deserialize';
+
+has log_cb => (
+ is => 'ro',
+ isa => CodeRef,
+ default => sub { sub {1} },
+);
+
+has content_type => (
+ is => 'ro',
+ isa => Str,
+ required => 1,
+ writer => 'set_content_type'
+);
+
+around serialize => sub {
+ my ( $orig, $self, $content, $options ) = @_;
+
+ blessed $self && $self->execute_hook( 'engine.serializer.before', $content );
+
+ $content or return $content;
+
+ my $data;
+ eval {
+ $data = $self->$orig( $content, $options );
+ blessed $self
+ and $self->execute_hook( 'engine.serializer.after', $data );
+ 1;
+ } or do {
+ my $error = $@ || 'Zombie Error';
+ blessed $self
+ and $self->log_cb->( core => "Failed to serialize content: $error" );
+ };
+
+ return $data;
+};
+
+around deserialize => sub {
+ my ( $orig, $self, $content, $options ) = @_;
+
+ $content && length $content > 0
+ or return $content;
+
+ my $data;
+ eval {
+ $data = $self->$orig($content, $options);
+ 1;
+ } or do {
+ my $error = $@ || 'Zombie Error';
+ $self->log_cb->( core => "Failed to deserialize content: $error" );
+ };
+
+ return $data;
+};
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::Serializer - Role for Serializer engines
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Any class that consumes this role will be able to be used as a
+serializer under Dancer2.
+
+In order to implement this role, the consumer B<must> implement the
+methods C<serialize> and C<deserialize>, and should define
+the C<content_type> attribute value.
+
+=head1 ATTRIBUTES
+
+=head2 content_type
+
+The I<content type> of the object after being serialized. For example,
+a JSON serializer would have a I<application/json> content type
+defined.
+
+=head1 METHODS
+
+=head2 serialize($content, [\%options])
+
+The serialize method need to be implemented by the consumer. It
+receives the serializer class object and a reference to the object to
+be serialized. Should return the object after being serialized, in the
+content type defined by the C<content_type> attribute.
+
+A third optional argument is a hash reference of options to the
+serializer.
+
+The serialize method must return bytes and therefore has to handle any
+encoding.
+
+=head2 deserialize($content, [\%options])
+
+The inverse method of C<serialize>. Receives the serializer class
+object and a string that should be deserialized. The method should
+return a reference to the deserialized Perl data structure.
+
+A third optional argument is a hash reference of options to the
+serializer.
+
+The deserialize method receives encoded bytes and must therefore
+handle any decoding required.
+
+=head1 CONFIGURATION
+
+The B<serializer> configuration variable tells Dancer2 which engine to use.
+
+You can change it either in your config.yml file:
+
+ #Set JSON engine
+ serializer: "JSON"
+
+ # Prettify JSON output
+ engines:
+ serializer:
+ JSON:
+ pretty: 1
+
+To know which engines are availables please see L<Dancer2::Manual/"Serializers">
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/SessionFactory.pm b/lib/Dancer2/Core/Role/SessionFactory.pm
new file mode 100644
index 00000000..53c33157
--- /dev/null
+++ b/lib/Dancer2/Core/Role/SessionFactory.pm
@@ -0,0 +1,503 @@
+package Dancer2::Core::Role::SessionFactory;
+# ABSTRACT: Role for session factories
+$Dancer2::Core::Role::SessionFactory::VERSION = '0.300000';
+use Moo::Role;
+with 'Dancer2::Core::Role::Engine';
+
+use Carp 'croak';
+use Dancer2::Core::Session;
+use Dancer2::Core::Types;
+use Digest::SHA 'sha1';
+use List::Util 'shuffle';
+use MIME::Base64 'encode_base64url';
+use Module::Runtime 'require_module';
+use Ref::Util qw< is_ref is_arrayref is_hashref >;
+
+sub hook_aliases { +{} }
+sub supported_hooks {
+ 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
+ /;
+}
+
+sub _build_type {
+ 'SessionFactory';
+} # XXX vs 'Session'? Unused, so I can't tell -- xdg
+
+has log_cb => (
+ is => 'ro',
+ isa => CodeRef,
+ default => sub { sub {1} },
+);
+
+has cookie_name => (
+ is => 'ro',
+ isa => Str,
+ default => sub {'dancer.session'},
+);
+
+has cookie_domain => (
+ is => 'ro',
+ isa => Str,
+ predicate => 1,
+);
+
+has cookie_path => (
+ is => 'ro',
+ isa => Str,
+ default => sub {"/"},
+);
+
+has cookie_duration => (
+ is => 'ro',
+ isa => Str,
+ predicate => 1,
+);
+
+has session_duration => (
+ is => 'ro',
+ isa => Num,
+ predicate => 1,
+);
+
+has is_secure => (
+ is => 'rw',
+ isa => Bool,
+ default => sub {0},
+);
+
+has is_http_only => (
+ is => 'rw',
+ isa => Bool,
+ default => sub {1},
+);
+
+sub create {
+ my ($self) = @_;
+
+ my %args = ( id => $self->generate_id, );
+
+ $args{expires} = $self->cookie_duration
+ if $self->has_cookie_duration;
+
+ my $session = Dancer2::Core::Session->new(%args);
+
+ $self->execute_hook( 'engine.session.before_create', $session );
+
+ # XXX why do we _flush now? Seems unnecessary -- xdg, 2013-03-03
+ eval { $self->_flush( $session->id, $session->data ) };
+ croak "Unable to create a new session: $@"
+ if $@;
+
+ $self->execute_hook( 'engine.session.after_create', $session );
+ return $session;
+}
+
+{
+ my $COUNTER = 0;
+ my $CPRNG_AVAIL = eval { require_module('Math::Random::ISAAC::XS'); 1; } &&
+ eval { require_module('Crypt::URandom'); 1; };
+
+ # don't initialize until generate_id is called so the ISAAC algorithm
+ # is seeded after any pre-forking
+ my $CPRNG;
+
+ # prepend epoch seconds so session ID is roughly monotonic
+ sub generate_id {
+ my ($self) = @_;
+
+ if ($CPRNG_AVAIL) {
+ $CPRNG ||= Math::Random::ISAAC::XS->new(
+ map { unpack( "N", Crypt::URandom::urandom(4) ) } 1 .. 256 );
+
+ # include $$ to ensure $CPRNG wasn't forked by accident
+ return encode_base64url(
+ pack(
+ "N6",
+ time, $$, $CPRNG->irand,
+ $CPRNG->irand, $CPRNG->irand, $CPRNG->irand
+ )
+ );
+ }
+ else {
+ my $seed = (
+ rand(1_000_000_000) # a random number
+ . __FILE__ # the absolute path as a secret key
+ . $COUNTER++ # impossible to have two consecutive dups
+ . $$ # the process ID as another private constant
+ . "$self" # the instance's memory address for more entropy
+ . join( '', shuffle( 'a' .. 'z', 'A' .. 'Z', 0 .. 9 ) )
+
+ # a shuffled list of 62 chars, another random component
+ );
+ return encode_base64url( pack( "Na*", time, sha1($seed) ) );
+ }
+
+ }
+}
+
+sub validate_id {
+ my ($self, $id) = @_;
+ return $id =~ m/^[A-Za-z0-9_\-~]+$/;
+}
+
+requires '_retrieve';
+
+sub retrieve {
+ my ( $self, %params ) = @_;
+ my $id = $params{id};
+
+ $self->execute_hook( 'engine.session.before_retrieve', $id );
+
+ my $data;
+ # validate format of session id before attempt to retrieve
+ my $rc = eval {
+ $self->validate_id($id) && ( $data = $self->_retrieve($id) );
+ };
+ croak "Unable to retrieve session with id '$id'"
+ if ! $rc;
+
+ my %args = ( id => $id, );
+
+ $args{data} = $data
+ if $data and is_hashref($data);
+
+ $args{expires} = $self->cookie_duration
+ if $self->has_cookie_duration;
+
+ my $session = Dancer2::Core::Session->new(%args);
+
+ $self->execute_hook( 'engine.session.after_retrieve', $session );
+ return $session;
+}
+
+# XXX eventually we could perhaps require '_change_id'?
+
+sub change_id {
+ my ( $self, %params ) = @_;
+ my $session = $params{session};
+ my $old_id = $session->id;
+
+ $self->execute_hook( 'engine.session.before_change_id', $old_id );
+
+ my $new_id = $self->generate_id;
+ $session->id( $new_id );
+
+ eval { $self->_change_id( $old_id, $new_id ) };
+ croak "Unable to change session id for session with id $old_id: $@"
+ if $@;
+
+ $self->execute_hook( 'engine.session.after_change_id', $new_id );
+}
+
+requires '_destroy';
+
+sub destroy {
+ my ( $self, %params ) = @_;
+ my $id = $params{id};
+ $self->execute_hook( 'engine.session.before_destroy', $id );
+
+ eval { $self->_destroy($id) };
+ croak "Unable to destroy session with id '$id': $@"
+ if $@;
+
+ $self->execute_hook( 'engine.session.after_destroy', $id );
+ return $id;
+}
+
+requires '_flush';
+
+sub flush {
+ my ( $self, %params ) = @_;
+ my $session = $params{session};
+ $self->execute_hook( 'engine.session.before_flush', $session );
+
+ eval { $self->_flush( $session->id, $session->data ) };
+ croak "Unable to flush session: $@"
+ if $@;
+
+ $self->execute_hook( 'engine.session.after_flush', $session );
+ return $session->id;
+}
+
+sub set_cookie_header {
+ my ( $self, %params ) = @_;
+ $params{response}->push_header(
+ 'Set-Cookie',
+ $self->cookie( session => $params{session} )->to_header
+ );
+}
+
+sub cookie {
+ my ( $self, %params ) = @_;
+ my $session = $params{session};
+ croak "cookie() requires a valid 'session' parameter"
+ unless is_ref($session) && $session->isa("Dancer2::Core::Session");
+
+ my %cookie = (
+ value => $session->id,
+ name => $self->cookie_name,
+ path => $self->cookie_path,
+ secure => $self->is_secure,
+ http_only => $self->is_http_only,
+ );
+
+ $cookie{domain} = $self->cookie_domain
+ if $self->has_cookie_domain;
+
+ if ( my $expires = $session->expires ) {
+ $cookie{expires} = $expires;
+ }
+
+ return Dancer2::Core::Cookie->new(%cookie);
+}
+
+requires '_sessions';
+
+sub sessions {
+ my ($self) = @_;
+ my $sessions = $self->_sessions;
+
+ croak "_sessions() should return an array ref"
+ unless is_arrayref($sessions);
+
+ return $sessions;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::SessionFactory - Role for session factories
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Any class that consumes this role will be able to store, create, retrieve and
+destroy session objects.
+
+The default values for attributes can be overridden in your Dancer2
+configuration. See L<Dancer2::Config/Session-engine>.
+
+=head1 ATTRIBUTES
+
+=head2 cookie_name
+
+The name of the cookie to create for storing the session key
+
+Defaults to C<dancer.session>
+
+=head2 cookie_domain
+
+The domain of the cookie to create for storing the session key.
+Defaults to the empty string and is unused as a result.
+
+=head2 cookie_path
+
+The path of the cookie to create for storing the session key.
+Defaults to "/".
+
+=head2 cookie_duration
+
+Default duration before session cookie expiration. If set, the
+L<Dancer2::Core::Session> C<expires> attribute will be set to the current time
+plus this duration (expression parsed by L<Dancer2::Core::Time>).
+
+=head2 session_duration
+
+Duration in seconds before sessions should expire, regardless of cookie
+expiration. If set, then SessionFactories should use this to enforce a limit
+on session validity.
+
+=head2 is_secure
+
+Boolean flag to tell if the session cookie is secure or not.
+
+Default is false.
+
+=head2 is_http_only
+
+Boolean flag to tell if the session cookie is http only.
+
+Default is true.
+
+=head1 INTERFACE
+
+Following is the interface provided by this role. When specified the required
+methods to implement are described.
+
+=head2 create
+
+Create a brand new session object and store it. Returns the newly created
+session object.
+
+Triggers an exception if the session is unable to be created.
+
+ my $session = MySessionFactory->create();
+
+This method does not need to be implemented in the class.
+
+=head2 generate_id
+
+Returns a randomly-generated, guaranteed-unique string.
+By default, it is a 32-character, URL-safe, Base64 encoded combination
+of a 32 bit timestamp and a 160 bit SHA1 digest of random seed data.
+The timestamp ensures that session IDs are generally monotonic.
+
+The default algorithm is not guaranteed cryptographically secure, but it's
+still reasonably strong for general use.
+
+If you have installed L<Math::Random::ISAAC::XS> and L<Crypt::URandom>,
+the seed data will be generated from a cryptographically-strong
+random number generator.
+
+This method is used internally by create() to set the session ID.
+
+This method does not need to be implemented in the class unless an
+alternative method for session ID generation is desired.
+
+=head2 validate_id
+
+Returns true if a session id is of the correct format, or false otherwise.
+
+By default, this ensures that the session ID is a string of characters
+from the Base64 schema for "URL Applications" plus the C<~> character.
+
+This method does not need to be implemented in the class unless an
+alternative set of characters for session IDs is desired.
+
+=head2 retrieve
+
+Return the session object corresponding to the session ID given. If none is
+found, triggers an exception.
+
+ my $session = MySessionFactory->retrieve(id => $id);
+
+The method C<_retrieve> must be implemented. It must take C<$id> as a single
+argument and must return a hash reference of session data.
+
+=head2 change_id
+
+Changes the session ID of the corresponding session.
+
+ MySessionFactory->change_id(session => $session_object);
+
+The method C<_change_id> must be implemented. It must take C<$old_id> and
+C<$new_id> as arguments and change the ID from the old one to the new one
+in the underlying session storage.
+
+=head2 destroy
+
+Purges the session object that matches the ID given. Returns the ID of the
+destroyed session if succeeded, triggers an exception otherwise.
+
+ MySessionFactory->destroy(id => $id);
+
+The C<_destroy> method must be implemented. It must take C<$id> as a single
+argument and destroy the underlying data.
+
+=head2 flush
+
+Make sure the session object is stored in the factory's backend. This method is
+called to notify the backend about the change in the session object.
+
+The Dancer application will not call flush unless the session C<is_dirty>
+attribute is true to avoid unnecessary writes to the database when no
+data has been modified.
+
+An exception is triggered if the session is unable to be updated in the backend.
+
+ MySessionFactory->flush(session => $session);
+
+The C<_flush> method must be implemented. It must take two arguments: the C<$id>
+and a hash reference of session data.
+
+=head2 set_cookie_header
+
+Sets the session cookie into the response object
+
+ MySessionFactory->set_cookie_header(
+ response => $response,
+ session => $session,
+ destroyed => undef,
+ );
+
+The C<response> parameter contains a L<Dancer2::Core::Response> object.
+The C<session> parameter contains a L<Dancer2::Core::Session> object.
+
+The C<destroyed> parameter is optional. If true, it indicates the
+session was marked destroyed by the request context. The default
+C<set_cookie_header> method doesn't need that information, but it is
+included in case a SessionFactory must handle destroyed sessions
+differently (such as signalling to middleware).
+
+=head2 cookie
+
+Coerce a session object into a L<Dancer2::Core::Cookie> object.
+
+ MySessionFactory->cookie(session => $session);
+
+=head2 sessions
+
+Return a list of all session IDs stored in the backend.
+Useful to create cleaning scripts, in conjunction with session's creation time.
+
+The C<_sessions> method must be implemented. It must return an array reference
+of session IDs (or an empty array reference).
+
+=head1 CONFIGURATION
+
+If there are configuration values specific to your session factory in your config.yml or
+environment, those will be passed to the constructor of the session factory automatically.
+In order to accept and store them, you need to define accessors for them.
+
+ engines:
+ session:
+ Example:
+ database_connection: "some_data"
+
+In your session factory:
+
+ package Dancer2::Session::Example;
+ use Moo;
+ with "Dancer2::Core::Role::SessionFactory";
+
+ has database_connection => ( is => "ro" );
+
+You need to do this for every configuration key. The ones that do not have accessors
+defined will just go to the void.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/SessionFactory/File.pm b/lib/Dancer2/Core/Role/SessionFactory/File.pm
new file mode 100644
index 00000000..dfab1a1d
--- /dev/null
+++ b/lib/Dancer2/Core/Role/SessionFactory/File.pm
@@ -0,0 +1,189 @@
+package Dancer2::Core::Role::SessionFactory::File;
+# ABSTRACT: Role for file-based session factories
+$Dancer2::Core::Role::SessionFactory::File::VERSION = '0.300000';
+use Moo::Role;
+with 'Dancer2::Core::Role::SessionFactory';
+
+use Carp 'croak';
+use Dancer2::Core::Types;
+use Dancer2::FileUtils qw(path set_file_mode escape_filename);
+use Fcntl ':flock';
+use File::Copy ();
+
+#--------------------------------------------------------------------------#
+# Required by classes consuming this role
+#--------------------------------------------------------------------------#
+
+requires '_suffix'; # '.yml', '.json', etc.
+requires '_thaw_from_handle'; # given handle, return session 'data' field
+requires '_freeze_to_handle'; # given handle and data, serialize it
+
+
+#--------------------------------------------------------------------------#
+# Attributes and methods
+#--------------------------------------------------------------------------#
+
+has session_dir => (
+ is => 'ro',
+ isa => Str,
+ default => sub { path( '.', 'sessions' ) },
+);
+
+sub BUILD {
+ my $self = shift;
+
+ if ( !-d $self->session_dir ) {
+ mkdir $self->session_dir
+ or croak "Unable to create session dir : "
+ . $self->session_dir . ' : '
+ . $!;
+ }
+}
+
+sub _sessions {
+ my ($self) = @_;
+ my $sessions = [];
+
+ opendir( my $dh, $self->session_dir )
+ or croak "Unable to open directory " . $self->session_dir . " : $!";
+
+ my $suffix = $self->_suffix;
+
+ while ( my $file = readdir($dh) ) {
+ next if $file eq '.' || $file eq '..';
+ if ( $file =~ /(\w+)\Q$suffix\E/ ) {
+ push @{$sessions}, $1;
+ }
+ }
+ closedir($dh);
+
+ return $sessions;
+}
+
+sub _retrieve {
+ my ( $self, $id ) = @_;
+ my $session_file = path( $self->session_dir, escape_filename($id) . $self->_suffix );
+
+ croak "Invalid session ID: $id" unless -f $session_file;
+
+ open my $fh, '+<', $session_file or die "Can't open '$session_file': $!\n";
+ flock $fh, LOCK_SH or die "Can't lock file '$session_file': $!\n";
+ my $data = $self->_thaw_from_handle($fh);
+ close $fh or die "Can't close '$session_file': $!\n";
+
+ return $data;
+}
+
+sub _change_id {
+ my ($self, $old_id, $new_id) = @_;
+
+ my $old_path =
+ path($self->session_dir, escape_filename($old_id) . $self->_suffix);
+
+ return if !-f $old_path;
+
+ my $new_path =
+ path($self->session_dir, escape_filename($new_id) . $self->_suffix);
+
+ File::Copy::move($old_path, $new_path);
+}
+
+sub _destroy {
+ my ( $self, $id ) = @_;
+ my $session_file = path( $self->session_dir, escape_filename($id) . $self->_suffix );
+ return if !-f $session_file;
+
+ unlink $session_file;
+}
+
+sub _flush {
+ my ( $self, $id, $data ) = @_;
+ my $session_file = path( $self->session_dir, escape_filename($id) . $self->_suffix );
+
+ open my $fh, '>', $session_file or die "Can't open '$session_file': $!\n";
+ flock $fh, LOCK_EX or die "Can't lock file '$session_file': $!\n";
+ seek $fh, 0, 0 or die "Can't seek in file '$session_file': $!\n";
+ truncate $fh, 0 or die "Can't truncate file '$session_file': $!\n";
+ set_file_mode($fh);
+ $self->_freeze_to_handle( $fh, $data );
+ close $fh or die "Can't close '$session_file': $!\n";
+
+ return $data;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::SessionFactory::File - Role for file-based session factories
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This is a specialized SessionFactory role for storing session
+data in files.
+
+This role manages the files. Classes consuming it only need to handle
+serialization and deserialization.
+
+Classes consuming this must satisfy three requirements: C<_suffix>,
+C<_freeze_to_handle> and C<_thaw_from_handle>.
+
+ package Dancer2::SessionFactory::XYX
+
+ use Moo;
+
+ has _suffix => (
+ is => 'ro',
+ isa => 'Str',
+ default => sub { '.xyz' },
+ );
+
+ with 'Dancer2::Core::Role::SessionFactory::File';
+
+ sub _freeze_to_handle {
+ my ($self, $fh, $data) = @_;
+
+ # ... do whatever to get data into $fh
+
+ return;
+ }
+
+ sub _thaw_from_handle {
+ my ($self, $fh) = @_;
+ my $data;
+
+ # ... do whatever to get data from $fh
+
+ return $data;
+ }
+
+ 1;
+
+=head1 ATTRIBUTES
+
+=head2 session_dir
+
+Where to store the session files. Defaults to "./sessions".
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/StandardResponses.pm b/lib/Dancer2/Core/Role/StandardResponses.pm
new file mode 100644
index 00000000..ad9efa3a
--- /dev/null
+++ b/lib/Dancer2/Core/Role/StandardResponses.pm
@@ -0,0 +1,70 @@
+package Dancer2::Core::Role::StandardResponses;
+# ABSTRACT: Role to provide commonly used responses
+$Dancer2::Core::Role::StandardResponses::VERSION = '0.300000';
+use Moo::Role;
+use Dancer2::Core::HTTP;
+
+sub response {
+ my ( $self, $app, $code, $message ) = @_;
+ $app->response->status($code);
+ $app->response->header( 'Content-Type', 'text/plain' );
+ return $message;
+}
+
+sub standard_response {
+ my ( $self, $app, $status_code ) = @_;
+
+ return $self->response(
+ $app,
+ $status_code,
+ Dancer2::Core::HTTP->status_message($status_code),
+ );
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::StandardResponses - Role to provide commonly used responses
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 METHODS
+
+=head2 response
+
+Generic method that produces a custom response given with a code and a message:
+
+ $self->response( $app, 404, 'Not Found' );
+
+This could be used to create your own, which is separate from the standard one:
+
+ $self->response( $app, 404, 'File missing in action' );
+
+=head2 standard_response
+
+Produces a standard response using the code.
+
+ # first example can be more easily written as
+ $self->standard_response( $app, 404 );
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Role/Template.pm b/lib/Dancer2/Core/Role/Template.pm
new file mode 100644
index 00000000..0d8a86c5
--- /dev/null
+++ b/lib/Dancer2/Core/Role/Template.pm
@@ -0,0 +1,367 @@
+# ABSTRACT: Role for template engines
+
+package Dancer2::Core::Role::Template;
+$Dancer2::Core::Role::Template::VERSION = '0.300000';
+use Dancer2::Core::Types;
+use Dancer2::FileUtils 'path';
+use Carp 'croak';
+use Ref::Util qw< is_ref >;
+
+use Moo::Role;
+with 'Dancer2::Core::Role::Engine';
+
+sub hook_aliases {
+ {
+ before_template_render => 'engine.template.before_render',
+ after_template_render => 'engine.template.after_render',
+ before_layout_render => 'engine.template.before_layout_render',
+ after_layout_render => 'engine.template.after_layout_render',
+ }
+}
+
+sub supported_hooks { values %{ shift->hook_aliases } }
+
+sub _build_type {'Template'}
+
+requires 'render';
+
+has log_cb => (
+ is => 'ro',
+ isa => CodeRef,
+ default => sub { sub {1} },
+);
+
+has name => (
+ is => 'ro',
+ lazy => 1,
+ builder => 1,
+);
+
+sub _build_name {
+ ( my $name = ref shift ) =~ s/^Dancer2::Template:://;
+ $name;
+}
+
+has charset => (
+ is => 'ro',
+ isa => Str,
+ default => sub {'UTF-8'},
+);
+
+has default_tmpl_ext => (
+ is => 'ro',
+ isa => Str,
+ default => sub { shift->config->{extension} || 'tt' },
+);
+
+has engine => (
+ is => 'ro',
+ isa => Object,
+ lazy => 1,
+ builder => 1,
+);
+
+has settings => (
+ is => 'ro',
+ isa => HashRef,
+ lazy => 1,
+ default => sub { +{} },
+ writer => 'set_settings',
+);
+
+# The attributes views, layout and layout_dir have triggers in
+# Dancer2::Core::App that enable their values to be modified by
+# the `set` keyword. As such, these are defined as read-write attrs.
+
+has views => (
+ is => 'rw',
+ isa => Maybe [Str],
+);
+
+has layout => (
+ is => 'rw',
+ isa => Maybe [Str],
+);
+
+has layout_dir => (
+ is => 'rw',
+ isa => Maybe [Str],
+);
+
+sub _template_name {
+ my ( $self, $view ) = @_;
+ my $def_tmpl_ext = $self->default_tmpl_ext();
+ $view .= ".$def_tmpl_ext" if $view !~ /\.\Q$def_tmpl_ext\E$/;
+ return $view;
+}
+
+sub view_pathname {
+ my ( $self, $view ) = @_;
+
+ $view = $self->_template_name($view);
+ return path( $self->views, $view );
+}
+
+sub layout_pathname {
+ my ( $self, $layout ) = @_;
+
+ return path(
+ $self->views,
+ $self->layout_dir,
+ $self->_template_name($layout),
+ );
+}
+
+sub pathname_exists {
+ my ( $self, $pathname ) = @_;
+ return -f $pathname;
+}
+
+sub render_layout {
+ my ( $self, $layout, $tokens, $content ) = @_;
+
+ $layout = $self->layout_pathname($layout);
+
+ # FIXME: not sure if I can "just call render"
+ $self->render( $layout, { %$tokens, content => $content } );
+}
+
+sub apply_renderer {
+ my ( $self, $view, $tokens ) = @_;
+ $view = $self->view_pathname($view) if !is_ref($view);
+ $tokens = $self->_prepare_tokens_options( $tokens );
+
+ $self->execute_hook( 'engine.template.before_render', $tokens );
+
+ my $content = $self->render( $view, $tokens );
+ $self->execute_hook( 'engine.template.after_render', \$content );
+
+ # make sure to avoid ( undef ) in list context return
+ defined $content and return $content;
+ return;
+}
+
+sub apply_layout {
+ my ( $self, $content, $tokens, $options ) = @_;
+
+ $tokens = $self->_prepare_tokens_options( $tokens );
+
+ # If 'layout' was given in the options hashref, use it if it's a true value,
+ # or don't use a layout if it was false (0, or undef); if layout wasn't
+ # given in the options hashref, go with whatever the current layout setting
+ # is.
+ my $layout =
+ exists $options->{layout}
+ ? ( $options->{layout} ? $options->{layout} : undef )
+ : ( $self->layout || $self->config->{layout} );
+
+ # that should only be $self->config, but the layout ain't there ???
+
+ defined $content or return;
+ defined $layout or return $content;
+
+ $self->execute_hook(
+ 'engine.template.before_layout_render',
+ $tokens, \$content
+ );
+
+ my $full_content = $self->render_layout( $layout, $tokens, $content );
+
+ $self->execute_hook( 'engine.template.after_layout_render',
+ \$full_content );
+
+ # make sure to avoid ( undef ) in list context return
+ defined $full_content and return $full_content;
+ return;
+}
+
+sub _prepare_tokens_options {
+ my ( $self, $tokens ) = @_;
+
+ # these are the default tokens provided for template processing
+ $tokens ||= {};
+ $tokens->{perl_version} = $^V;
+ $tokens->{dancer_version} = Dancer2->VERSION;
+ $tokens->{settings} = $self->settings;
+
+ # no request when template is called as a global keyword
+ if ( $self->has_request ) {
+ $tokens->{request} = $self->request;
+ $tokens->{params} = $self->request->params;
+ $tokens->{vars} = $self->request->vars;
+
+ # a session can not exist if there is no request
+ $tokens->{session} = $self->session->data
+ if $self->has_session;
+ }
+
+ return $tokens;
+}
+
+sub process {
+ my ( $self, $view, $tokens, $options ) = @_;
+ my ( $content, $full_content );
+
+ # it's important that $tokens is not undef, so that things added to it via
+ # a before_template in apply_renderer survive to the apply_layout. GH#354
+ $tokens ||= {};
+ $options ||= {};
+
+ ## FIXME - Look into PR 654 so we fix the problem here as well!
+
+ $content =
+ $view
+ ? $self->apply_renderer( $view, $tokens )
+ : delete $options->{content};
+
+ defined $content
+ and $full_content = $self->apply_layout( $content, $tokens, $options );
+
+ defined $full_content
+ and return $full_content;
+
+ croak "Template did not produce any content";
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Role::Template - Role for template engines
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Any class that consumes this role will be able to be used as a template engine
+under Dancer2.
+
+In order to implement this role, the consumer B<must> implement the method C<render>. This method will receive three arguments:
+
+=over 4
+
+=item $self
+
+=item $template
+
+=item $tokens
+
+=back
+
+Any template receives the following tokens, by default:
+
+=over 4
+
+=item * C<perl_version>
+
+Current version of perl, effectively C<$^V>.
+
+=item * C<dancer_version>
+
+Current version of Dancer2, effectively C<< Dancer2->VERSION >>.
+
+=item * C<settings>
+
+A hash of the application configuration.
+
+=item * C<request>
+
+The current request object.
+
+=item * C<params>
+
+A hash reference of all the parameters.
+
+Currently the equivalent of C<< $request->params >>.
+
+=item * C<vars>
+
+The list of request variables, which is what you would get if you
+called the C<vars> keyword.
+
+=item * C<session>
+
+The current session data, if a session exists.
+
+=back
+
+=head1 ATTRIBUTES
+
+=head2 name
+
+The name of the template engine (e.g.: Simple).
+
+=head2 charset
+
+The charset. The default value is B<UTF-8>.
+
+=head2 default_tmpl_ext
+
+The default file extension. If not provided, B<tt> is used.
+
+=head2 views
+
+Path to the directory containing the views.
+
+=head2 layout
+
+Path to the directory containing the layouts.
+
+=head2 layout_dir
+
+Relative path to the layout directory.
+
+Default: B<layouts>.
+
+=head2 engine
+
+Contains the engine.
+
+=head1 METHODS
+
+=head2 view_pathname($view)
+
+Returns the full path to the requested view.
+
+=head2 layout_pathname($layout)
+
+Returns the full path to the requested layout.
+
+=head2 pathname_exists($pathname)
+
+Returns true if the requested pathname exists. Can be used for either views
+or layouts:
+
+ $self->pathname_exists( $self->view_pathname( 'some_view' ) );
+ $self->pathname_exists( $self->layout_pathname( 'some_layout' ) );
+
+=head2 render_layout($layout, \%tokens, \$content)
+
+Render the layout with the applied tokens
+
+=head2 apply_renderer($view, \%tokens)
+
+=head2 apply_layout($content, \%tokens, \%options)
+
+=head2 process($view, \%tokens, \%options)
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Route.pm b/lib/Dancer2/Core/Route.pm
new file mode 100644
index 00000000..388bb0e1
--- /dev/null
+++ b/lib/Dancer2/Core/Route.pm
@@ -0,0 +1,373 @@
+package Dancer2::Core::Route;
+# ABSTRACT: Dancer2's route handler
+$Dancer2::Core::Route::VERSION = '0.300000';
+use Moo;
+use Dancer2::Core::Types;
+use Module::Runtime 'use_module';
+use Carp 'croak';
+use List::Util 'first';
+use Scalar::Util 'blessed';
+use Ref::Util qw< is_regexpref >;
+use Type::Registry;
+
+our ( $REQUEST, $RESPONSE, $RESPONDER, $WRITER, $ERROR_HANDLER );
+
+has method => (
+ is => 'ro',
+ isa => Dancer2Method,
+ required => 1,
+);
+
+has code => (
+ is => 'ro',
+ required => 1,
+ isa => CodeRef,
+);
+
+has regexp => (
+ is => 'ro',
+ required => 1,
+);
+
+has spec_route => ( is => 'ro' );
+
+has prefix => (
+ is => 'ro',
+ isa => Maybe [Dancer2Prefix],
+ predicate => 1,
+);
+
+has options => (
+ is => 'ro',
+ isa => HashRef,
+ trigger => \&_check_options,
+ predicate => 1,
+);
+
+sub _check_options {
+ my ( $self, $options ) = @_;
+ return 1 unless defined $options;
+
+ my @supported_options = (
+ qw/content_type agent user_agent content_length
+ path_info/
+ );
+ for my $opt ( keys %{$options} ) {
+ croak "Not a valid option for route matching: `$opt'"
+ if not( grep {/^$opt$/} @supported_options );
+ }
+ return 1;
+}
+
+# private attributes
+
+has _should_capture => (
+ is => 'ro',
+ isa => Bool,
+);
+
+has _match_data => (
+ is => 'rw',
+ isa => HashRef,
+);
+
+has _params => (
+ is => 'ro',
+ isa => ArrayRef,
+ default => sub { [] },
+);
+
+has _typed_params => (
+ is => 'ro',
+ isa => ArrayRef,
+ default => sub { [] },
+);
+
+sub match {
+ my ( $self, $request ) = @_;
+
+ if ( $self->has_options ) {
+ return unless $self->validate_options($request);
+ }
+
+ my @values = $request->path =~ $self->regexp;
+
+ return unless @values;
+
+ # if some named captures are found, return captures
+ # no warnings is for perl < 5.10
+ # - Note no @values implies no named captures
+ if (my %captures =
+ do { no warnings; %+ }
+ )
+ {
+ return $self->_match_data( { captures => \%captures } );
+ }
+
+ # regex comments are how we know if we captured a token,
+ # splat or a megasplat
+ my @token_or_splat =
+ $self->regexp =~ /\(\?#((?:typed_)?token|(?:mega)?splat)\)/g;
+
+ if (@token_or_splat) {
+ # our named tokens
+ my @tokens = @{ $self->_params };
+ my @typed_tokens = @{ $self->_typed_params };
+
+ my %params;
+ my @splat;
+ for ( my $i = 0; $i < @values; $i++ ) {
+ # Is this value from a token?
+ if ( $token_or_splat[$i] eq 'typed_token' ) {
+ my ( $token, $type ) = @{ shift @typed_tokens };
+
+ if (defined $values[$i]) {
+ # undef value mean that token was marked as optional so
+ # we only do type check on defined value
+ return
+ unless $type->check($values[$i]);
+ }
+ $params{$token} = $values[$i];
+ next;
+ }
+ if ( $token_or_splat[$i] eq 'token' ) {
+ $params{ shift @tokens } = $values[$i];
+ next;
+ }
+
+ # megasplat values are split on '/'
+ if ($token_or_splat[$i] eq 'megasplat') {
+ $values[$i] = [
+ defined $values[$i] ? split( m{/} , $values[$i], -1 ) : ()
+ ];
+ }
+ push @splat, $values[$i];
+ }
+ return $self->_match_data( {
+ %params,
+ (splat => \@splat)x!! @splat,
+ });
+ }
+
+ if ( $self->_should_capture ) {
+ return $self->_match_data( { splat => \@values } );
+ }
+
+ return $self->_match_data( {} );
+}
+
+sub execute {
+ my ( $self, $app, @args ) = @_;
+ local $REQUEST = $app->request;
+ local $RESPONSE = $app->response;
+
+ my $content = $self->code->( $app, @args );
+
+ # users may set content in the response. If the response has
+ # content, and the returned value from the route code is not
+ # an object (well, reference) we ignore the returned value
+ # and use the existing content in the response instead.
+ $RESPONSE->has_content && !ref $content
+ and return $app->_prep_response( $RESPONSE );
+
+ my $type = blessed($content)
+ or return $app->_prep_response( $RESPONSE, $content );
+
+ # Plack::Response: proper ArrayRef-style response
+ $type eq 'Plack::Response'
+ and $RESPONSE = Dancer2::Core::Response->new_from_plack($RESPONSE);
+
+ # CodeRef: raw PSGI response
+ # do we want to allow it and forward it back?
+ # do we want to upgrade it to an asynchronous response?
+ $type eq 'CODE'
+ and die "We do not support returning code references from routes.\n";
+
+ # Dancer2::Core::Response, Dancer2::Core::Response::Delayed:
+ # proper responses
+ $type eq 'Dancer2::Core::Response'
+ and return $RESPONSE;
+
+ $type eq 'Dancer2::Core::Response::Delayed'
+ and return $content;
+
+ # we can't handle arrayref or hashref
+ # because those might be serialized back
+ die "Unrecognized response type from route: $type.\n";
+}
+
+# private subs
+
+sub BUILDARGS {
+ my ( $class, %args ) = @_;
+
+ my $prefix = $args{prefix};
+ my $regexp = $args{regexp};
+
+ my $type_library = delete $args{type_library};
+ if ( $type_library) {
+ eval { use_module($type_library); 1 }
+ or croak "type_library $type_library cannot be loaded";
+ }
+ $type_library ||= 'Dancer2::Core::Types';
+
+ # init prefix
+ if ( $prefix ) {
+ $args{regexp} =
+ is_regexpref($regexp) ? qr{^\Q${prefix}\E${regexp}$} :
+ $prefix . $regexp;
+ }
+ elsif ( !is_regexpref($regexp) ) {
+ # No prefix, so ensure regexp begins with a '/'
+ index( $regexp, '/', 0 ) == 0 or $args{regexp} = "/$regexp";
+ }
+
+ # init regexp
+ $regexp = $args{regexp}; # updated value
+ $args{spec_route} = $regexp;
+
+ if ( is_regexpref($regexp)) {
+ $args{_should_capture} = 1;
+ }
+ else {
+ @args{qw/ regexp _params _typed_params _should_capture/} =
+ @{ _build_regexp_from_string($regexp, $type_library) };
+ }
+
+ return \%args;
+}
+
+sub _build_regexp_from_string {
+ my ($string, $type_library) = @_;
+
+ my $capture = 0;
+ my ( @params, @typed_params );
+
+ my $type_registry = Type::Registry->new;
+ $type_registry->add_types($type_library);
+
+ # look for route with tokens [aka params] (/hello/:foo)
+ if ( $string =~ /:/ ) {
+ my @found = $string =~ m|:([^/.\?]+)|g;
+ foreach my $token ( @found ) {
+ if ( $token =~ s/\[(.+)\]$// ) {
+
+ # typed token
+ my $type = $type_registry->lookup($1);
+ push @typed_params, [ $token, $type ];
+ }
+ else {
+ push @params, $token;
+ }
+ }
+ if (@typed_params) {
+ $string =~ s!(:[^/.\?]+\[[^/.\?]+\])!(?#typed_token)([^/]+)!g;
+ $capture = 1;
+ }
+ if (@params) {
+ first { $_ eq 'splat' } @params
+ and warn q{Named placeholder 'splat' is deprecated};
+
+ first { $_ eq 'captures' } @params
+ and warn q{Named placeholder 'captures' is deprecated};
+
+ $string =~ s!(:[^\/\.\?]+)!(?#token)([^/]+)!g;
+ $capture = 1;
+ }
+ }
+
+ # parse megasplat
+ # we use {0,} instead of '*' not to fall in the splat rule
+ # same logic for [^\n] instead of '.'
+ $capture = 1 if $string =~ s!\Q**\E!(?#megasplat)([^\n]+)!g;
+
+ # parse wildcards
+ $capture = 1 if $string =~ s!\*!(?#splat)([^/]+)!g;
+
+ # escape dots
+ $string =~ s/\./\\\./g if $string =~ /\./;
+
+ # escape slashes
+ $string =~ s/\//\\\//g;
+
+ return [ "^$string\$", \@params, \@typed_params, $capture ];
+}
+
+sub validate_options {
+ my ( $self, $request ) = @_;
+
+ for my $option ( keys %{ $self->options } ) {
+ return 0
+ if (
+ ( not $request->$option )
+ || ( $request->$option !~ $self->options->{ $option } )
+ )
+ }
+ return 1;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Route - Dancer2's route handler
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 ATTRIBUTES
+
+=head2 method
+
+The HTTP method of the route (lowercase). Required.
+
+=head2 code
+
+The code reference to execute when the route is ran. Required.
+
+=head2 regexp
+
+The regular expression that defines the path of the route.
+Required. Coerce from Dancer2's route I<patterns>.
+
+=head2 prefix
+
+The prefix to prepend to the C<regexp>. Optional.
+
+=head2 options
+
+A HashRef of conditions on which the matching will depend. Optional.
+
+=head1 METHODS
+
+=head2 match
+
+Try to match the route with a given L<Dancer2::Core::Request> object.
+Returns the hash of matching data if success (captures and values of the route
+against the path of the request) or C<undef> if not.
+
+ my $match = $route->match( $request );
+
+=head2 execute
+
+Runs the coderef of the route.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Runner.pm b/lib/Dancer2/Core/Runner.pm
new file mode 100644
index 00000000..9f310040
--- /dev/null
+++ b/lib/Dancer2/Core/Runner.pm
@@ -0,0 +1,277 @@
+package Dancer2::Core::Runner;
+# ABSTRACT: Top-layer class to start a dancer app
+$Dancer2::Core::Runner::VERSION = '0.300000';
+use Moo;
+use Carp 'croak';
+use Module::Runtime 'require_module';
+use Dancer2::Core::MIME;
+use Dancer2::Core::Types;
+use Dancer2::Core::Dispatcher;
+use Plack::Builder qw();
+use Ref::Util qw< is_ref is_regexpref >;
+
+# Hashref of configurable items for the runner.
+# Defaults come from ENV vars. Updated via global triggers
+# from app configs.
+has config => (
+ is => 'ro',
+ isa => HashRef,
+ lazy => 1,
+ builder => '_build_config',
+);
+
+# FIXME: i hate this
+has mime_type => (
+ is => 'ro',
+ isa => InstanceOf ['Dancer2::Core::MIME'],
+ default => sub { Dancer2::Core::MIME->new(); },
+);
+
+has server => (
+ is => 'ro',
+ isa => InstanceOf['HTTP::Server::PSGI'],
+ lazy => 1,
+ builder => '_build_server',
+ handles => ['run'],
+);
+
+has apps => (
+ is => 'ro',
+ isa => ArrayRef,
+ default => sub { [] },
+);
+
+has postponed_hooks => (
+ is => 'ro',
+ isa => HashRef,
+ default => sub { +{} },
+);
+
+has environment => (
+ is => 'ro',
+ isa => Str,
+ required => 1,
+ default => sub {
+ $ENV{DANCER_ENVIRONMENT} || $ENV{PLACK_ENV} || 'development'
+ },
+);
+
+has host => (
+ is => 'ro',
+ lazy => 1,
+ default => sub { $_[0]->config->{'host'} },
+);
+
+has port => (
+ is => 'ro',
+ lazy => 1,
+ default => sub { $_[0]->config->{'port'} },
+);
+
+has timeout => (
+ is => 'ro',
+ lazy => 1,
+ default => sub { $_[0]->config->{'timeout'} },
+);
+
+sub _build_server {
+ my $self = shift;
+
+ require_module('HTTP::Server::PSGI');
+ HTTP::Server::PSGI->new(
+ host => $self->host,
+ port => $self->port,
+ timeout => $self->timeout,
+ server_software => "Perl Dancer2 " . Dancer2->VERSION,
+ );
+}
+
+sub _build_config {
+ my $self = shift;
+
+ $ENV{PLACK_ENV}
+ and $ENV{DANCER_APPHANDLER} = 'PSGI';
+
+ return {
+ behind_proxy => 0,
+ apphandler => ( $ENV{DANCER_APPHANDLER} || 'Standalone' ),
+ traces => ( $ENV{DANCER_TRACES} || 0 ),
+ host => ( $ENV{DANCER_SERVER} || '0.0.0.0' ),
+ port => ( $ENV{DANCER_PORT} || '3000' ),
+ no_server_tokens => ( defined $ENV{DANCER_NO_SERVER_TOKENS} ?
+ $ENV{DANCER_NO_SERVER_TOKENS} :
+ 0 ),
+ startup_info => ( defined $ENV{DANCER_STARTUP_INFO} ?
+ $ENV{DANCER_STARTUP_INFO} :
+ 1 ),
+ };
+}
+
+sub BUILD {
+ my $self = shift;
+
+ # Enable traces if set by ENV var.
+ if (my $traces = $self->config->{traces} ) {
+ require_module('Carp');
+ $Carp::Verbose = $traces ? 1 : 0;
+ };
+
+ # set the global runner object if one doesn't exist yet
+ # this can happen if you create one without going through Dancer2
+ # which doesn't trigger the import that creates it
+ defined $Dancer2::runner
+ or $Dancer2::runner = $self;
+}
+
+sub register_application {
+ my $self = shift;
+ my $app = shift;
+
+ push @{ $self->apps }, $app;
+
+ # add postponed hooks to our psgi app
+ $self->add_postponed_hooks( $app->name, $app->postponed_hooks );
+}
+
+sub add_postponed_hooks {
+ my $self = shift;
+ my $name = shift;
+ my $hooks = shift;
+
+ # merge postponed hooks
+ @{ $self->{'postponed_hooks'}{$name} }{ keys %{$hooks} } = values %{$hooks};
+}
+
+# decide what to start
+# do we just return a PSGI app
+# or do we actually start a development standalone server?
+sub start {
+ my $self = shift;
+ my $app = $self->psgi_app;
+
+ # we decide whether we return a PSGI coderef
+ # or spin a local development PSGI server
+ $self->config->{'apphandler'} eq 'PSGI'
+ and return $app;
+
+ # FIXME: this should not include the server tokens
+ # since those are already added to the server itself
+ $self->start_server($app);
+}
+
+sub start_server {
+ my $self = shift;
+ my $app = shift;
+
+ # does not return
+ $self->print_banner;
+ $self->server->run($app);
+}
+
+sub psgi_app {
+ my ($self, $apps) = @_;
+
+ if ( $apps && @{$apps} ) {
+ my @found_apps = ();
+
+ foreach my $app_req ( @{$apps} ) {
+ if ( is_regexpref($app_req) ) {
+ # find it in the apps registry
+ push @found_apps,
+ grep +( $_->name =~ $app_req ), @{ $self->apps };
+ } elsif ( ref $app_req eq 'Dancer2::Core::App' ) {
+ # use it directly
+ push @found_apps, $app_req;
+ } elsif ( !is_ref($app_req) ) {
+ # find it in the apps registry
+ push @found_apps,
+ grep +( $_->name eq $app_req ), @{ $self->apps };
+ } else {
+ croak "Invalid input to psgi_app: $app_req";
+ }
+ }
+
+ $apps = \@found_apps;
+ } else {
+ # dispatch over all apps by default
+ $apps = $self->apps;
+ }
+
+ my $dispatcher = Dancer2::Core::Dispatcher->new( apps => $apps );
+
+ # initialize psgi_apps
+ # (calls ->finish on the apps and create their PSGI apps)
+ # the dispatcher caches that in the attribute
+ # so ->finish isn't actually called again if you run this method
+ $dispatcher->apps_psgi;
+
+ return sub {
+ my $env = shift;
+
+ # mark it as an old-style dispatching
+ $self->{'internal_dispatch'} = 1;
+
+ my $response = $dispatcher->dispatch($env);
+
+ # unmark it
+ delete $self->{'internal_dispatch'};
+
+ # cleanup
+ delete $self->{'internal_sessions'};
+
+ return $response;
+ };
+}
+
+sub print_banner {
+ my $self = shift;
+ my $pid = $$;
+
+ # we only print the info if we need to
+ $self->config->{'startup_info'} or return;
+
+ # bare minimum
+ print STDERR ">> Dancer2 v" . Dancer2->VERSION . " server $pid listening "
+ . 'on http://'
+ . $self->host . ':'
+ . $self->port . "\n";
+
+ # all loaded plugins
+ foreach my $module ( grep { $_ =~ m{^Dancer2/Plugin/} } keys %INC ) {
+ $module =~ s{/}{::}g; # change / to ::
+ $module =~ s{\.pm$}{}; # remove .pm at the end
+ my $version = $module->VERSION;
+
+ defined $version or $version = 'no version number defined';
+ print STDERR ">> $module ($version)\n";
+ }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Runner - Top-layer class to start a dancer app
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Session.pm b/lib/Dancer2/Core/Session.pm
new file mode 100644
index 00000000..64a24b01
--- /dev/null
+++ b/lib/Dancer2/Core/Session.pm
@@ -0,0 +1,153 @@
+package Dancer2::Core::Session;
+# ABSTRACT: class to represent any session object
+$Dancer2::Core::Session::VERSION = '0.300000';
+use Moo;
+use Dancer2::Core::Types;
+use Dancer2::Core::Time;
+
+has id => (
+ # for some specific plugins this should be rw.
+ # refer to https://github.com/PerlDancer/Dancer2/issues/460
+ is => 'rw',
+ isa => Str,
+ required => 1,
+);
+
+has data => (
+ is => 'ro',
+ lazy => 1,
+ default => sub { {} },
+);
+
+has expires => (
+ is => 'rw',
+ isa => Str,
+ coerce => sub {
+ my $value = shift;
+ $value += time if $value =~ /^[\-\+]?\d+$/;
+ Dancer2::Core::Time->new( expression => $value )->epoch;
+ },
+);
+
+has is_dirty => (
+ is => 'rw',
+ isa => Bool,
+ default => sub {0},
+);
+
+
+sub read {
+ my ( $self, $key ) = @_;
+ return $self->data->{$key};
+}
+
+
+sub write {
+ my ( $self, $key, $value ) = @_;
+ $self->is_dirty(1);
+ $self->data->{$key} = $value;
+}
+
+sub delete {
+ my ( $self, $key, $value ) = @_;
+ $self->is_dirty(1);
+ delete $self->data->{$key};
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Session - class to represent any session object
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+A session object encapsulates anything related to a specific session: its ID,
+its data, and its expiration.
+
+It is completely agnostic of how it will be stored, this is the role of
+a factory that consumes L<Dancer2::Core::Role::SessionFactory> to know about that.
+
+Generally, session objects should not be created directly. The correct way to
+get a new session object is to call the C<create()> method on a session engine
+that implements the SessionFactory role. This is done automatically by the
+app object if a session engine is defined.
+
+=head1 ATTRIBUTES
+
+=head2 id
+
+The identifier of the session object. Required. By default,
+L<Dancer2::Core::Role::SessionFactory> sets this to a randomly-generated,
+guaranteed-unique string.
+
+This attribute can be modified if your Session implementation requires this.
+
+=head2 data
+
+Contains the data of the session (Hash).
+
+=head2 expires
+
+Number of seconds for the expiry of the session cookie. Don't add the current
+timestamp to it, will be done automatically.
+
+Default is no expiry (session cookie will leave for the whole browser's
+session).
+
+For a lifetime of one hour:
+
+ expires => 3600
+
+=head2 is_dirty
+
+Boolean value for whether data in the session has been modified.
+
+=head1 METHODS
+
+=head2 read
+
+Reader on the session data
+
+ my $value = $session->read('something');
+
+Returns C<undef> if the key does not exist in the session.
+
+=head2 write
+
+Writer on the session data
+
+ $session->write('something', $value);
+
+Sets C<is_dirty> to true. Returns C<$value>.
+
+=head2 delete
+
+Deletes a key from session data
+
+ $session->delete('something');
+
+Sets C<is_dirty> to true. Returns the value deleted from the session.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Time.pm b/lib/Dancer2/Core/Time.pm
new file mode 100644
index 00000000..5296b0e4
--- /dev/null
+++ b/lib/Dancer2/Core/Time.pm
@@ -0,0 +1,201 @@
+package Dancer2::Core::Time;
+# ABSTRACT: class to handle common helpers for time manipulations
+$Dancer2::Core::Time::VERSION = '0.300000';
+use Moo;
+
+has seconds => (
+ is => 'ro',
+ lazy => 1,
+ builder => '_build_seconds',
+);
+
+sub _build_seconds {
+ my ($self) = @_;
+ my $seconds = $self->expression;
+
+ return $seconds
+ if $seconds =~ /^\d+$/;
+
+ return $self->_parse_duration($seconds)
+}
+
+has epoch => (
+ is => 'ro',
+ lazy => 1,
+ builder => '_build_epoch',
+);
+
+sub _build_epoch {
+ my ($self) = @_;
+ return $self->seconds if $self->seconds !~ /^[\-\+]?\d+$/;
+ $self->seconds + time;
+}
+
+has gmt_string => (
+ is => 'ro',
+ builder => '_build_gmt_string',
+ lazy => 1,
+);
+
+sub _build_gmt_string {
+ my ($self) = @_;
+ my $epoch = $self->epoch;
+ return $epoch if $epoch !~ /^\d+$/;
+
+ my ( $sec, $min, $hour, $mday, $mon, $year, $wday ) = gmtime($epoch);
+ my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+ my @days = qw(Sun Mon Tue Wed Thu Fri Sat);
+
+ return sprintf "%s, %02d-%s-%d %02d:%02d:%02d GMT",
+ $days[$wday],
+ $mday,
+ $months[$mon],
+ ( $year + 1900 ),
+ $hour, $min, $sec;
+}
+
+has expression => (
+ is => 'ro',
+ required => 1,
+);
+
+sub BUILDARGS {
+ my ($class, %args) = @_;
+
+ $args{epoch} = $args{expression}
+ if $args{expression} =~ /^\d+$/;
+
+ return \%args;
+}
+
+# private
+
+# This map is taken from Cache and Cache::Cache
+# map of expiration formats to their respective time in seconds
+#<<< no perl tidy
+my %Units = ( map(($_, 1), qw(s second seconds sec secs)),
+ map(($_, 60), qw(m minute minutes min mins)),
+ map(($_, 60*60), qw(h hr hour hours)),
+ map(($_, 60*60*24), qw(d day days)),
+ map(($_, 60*60*24*7), qw(w week weeks)),
+ map(($_, 60*60*24*30), qw(M month months)),
+ map(($_, 60*60*24*365), qw(y year years)) );
+#>>>
+
+# This code is taken from Time::Duration::Parse, except if it isn't
+# understood it just passes it through and it adds the current time.
+sub _parse_duration {
+ my ( $self, $timespec ) = @_;
+ my $orig_timespec = $timespec;
+
+ # Treat a plain number as a number of seconds (and parse it later)
+ if ( $timespec =~ /^\s*([-+]?\d+(?:[.,]\d+)?)\s*$/ ) {
+ $timespec = "$1s";
+ }
+
+ # Convert hh:mm(:ss)? to something we understand
+ $timespec =~ s/\b(\d+):(\d\d):(\d\d)\b/$1h $2m $3s/g;
+ $timespec =~ s/\b(\d+):(\d\d)\b/$1h $2m/g;
+
+ my $duration = 0;
+ while ( $timespec
+ =~ s/^\s*([-+]?\d+(?:[.,]\d+)?)\s*([a-zA-Z]+)(?:\s*(?:,|and)\s*)*//i )
+ {
+ my ( $amount, $unit ) = ( $1, $2 );
+ $unit = lc($unit) unless length($unit) == 1;
+
+ if ( my $value = $Units{$unit} ) {
+ $amount =~ s/,/./;
+ $duration += $amount * $value;
+ }
+ else {
+ return $orig_timespec;
+ }
+ }
+
+ if ( $timespec =~ /\S/ ) {
+ return $orig_timespec;
+ }
+
+ return sprintf "%.0f", $duration;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Time - class to handle common helpers for time manipulations
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ my $time = Dancer2::Core::Time->new( expression => "1h" );
+ $time->seconds; # return 3600
+
+=head1 DESCRIPTION
+
+For consistency, whenever something needs to work with time, it
+needs to be expressed in seconds, with a timestamp. Although it's very
+convenient for the machine and calculations, it's not very handy for a
+human-being, for instance in a configuration file.
+
+This class provides everything needed to translate any human-understandable
+expression into a number of seconds.
+
+=head1 ATTRIBUTES
+
+=head2 seconds
+
+Number of seconds represented by the object. Defaults to 0.
+
+=head2 epoch
+
+The current epoch to handle. Defaults to seconds + time.
+
+=head2 gmt_string
+
+Convert the current value in epoch as a GMT string.
+
+=head2 expression
+
+Required. A human readable expression representing the number of seconds to provide.
+
+The format supported is a number followed by an expression. It currently
+understands:
+
+ s second seconds sec secs
+ m minute minutes min mins
+ h hr hour hours
+ d day days
+ w week weeks
+ M month months
+ y year years
+
+Months and years are currently fixed at 30 and 365 days. This may change.
+Anything else is used verbatim as the expression of a number of seconds.
+
+Example:
+
+ 2 hours, 3 days, 3d, 1 week, 3600, etc...
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Core/Types.pm b/lib/Dancer2/Core/Types.pm
new file mode 100644
index 00000000..3133e97c
--- /dev/null
+++ b/lib/Dancer2/Core/Types.pm
@@ -0,0 +1,149 @@
+package Dancer2::Core::Types;
+# ABSTRACT: Type::Tiny types for Dancer2 core.
+$Dancer2::Core::Types::VERSION = '0.300000';
+use strict;
+use warnings;
+use Type::Library -base;
+use Type::Utils -all;
+use Sub::Quote 'quote_sub';
+
+BEGIN { extends "Types::Standard" };
+
+our %supported_http_methods = map +( $_ => 1 ), qw<
+ GET HEAD POST PUT DELETE OPTIONS PATCH
+>;
+
+my $single_part = qr/
+ [A-Za-z] # must start with letter
+ (?: [A-Za-z0-9_]+ )? # can continue with letters, numbers or underscore
+/x;
+
+my $namespace = qr/
+ ^
+ $single_part # first part
+ (?: (?: \:\: $single_part )+ )? # optional part starting with double colon
+ $
+/x;
+
+declare 'ReadableFilePath', constraint => quote_sub q{ -e $_ && -r $_ };
+
+declare 'WritableFilePath', constraint => quote_sub q{ -e $_ && -w $_ };
+
+declare 'Dancer2Prefix', as 'Str', where {
+ # a prefix must start with the char '/'
+ # index is much faster than =~ /^\//
+ index($_, '/') == 0
+};
+
+declare 'Dancer2AppName', as 'Str', where {
+ # TODO need a real check of valid app names
+ $_ =~ $namespace;
+}, message {
+ sprintf("%s is not a Dancer2AppName",
+ ($_ && length($_)) ? $_ : 'Empty string')
+};
+
+declare 'Dancer2Method', as Enum [map +(lc), keys %supported_http_methods];
+
+declare 'Dancer2HTTPMethod', as Enum [keys %supported_http_methods];
+
+# generate abbreviated class types for core dancer objects
+for my $type (
+ qw/
+ App
+ Context
+ Cookie
+ DSL
+ Dispatcher
+ Error
+ Hook
+ MIME
+ Request
+ Response
+ Role
+ Route
+ Runner
+ Server
+ Session
+ Types
+ /
+ )
+{
+ declare $type,
+ as InstanceOf[ 'Dancer2::Core::' . $type ];
+}
+
+# Export everything by default.
+our @EXPORT = __PACKAGE__->type_names;
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Core::Types - Type::Tiny types for Dancer2 core.
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+L<Type::Tiny> definitions for Moo attributes. These are defined as subroutines.
+
+=head1 MOO TYPES
+
+=head2 ReadableFilePath($value)
+
+A readable file path.
+
+=head2 WritableFilePath($value)
+
+A writable file path.
+
+=head2 Dancer2Prefix($value)
+
+A proper Dancer2 prefix, which is basically a prefix that starts with a I</>
+character.
+
+=head2 Dancer2AppName($value)
+
+A proper Dancer2 application name.
+
+Currently this only checks for I<\w+>.
+
+=head2 Dancer2Method($value)
+
+An acceptable method supported by Dancer2.
+
+Currently this includes: I<get>, I<head>, I<post>, I<put>, I<delete> and
+I<options>.
+
+=head2 Dancer2HTTPMethod($value)
+
+An acceptable HTTP method supported by Dancer2.
+
+Current this includes: I<GET>, I<HEAD>, I<POST>, I<PUT>, I<DELETE>
+and I<OPTIONS>.
+
+=head1 SEE ALSO
+
+L<Types::Standard> for more available types
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/FileUtils.pm b/lib/Dancer2/FileUtils.pm
new file mode 100644
index 00000000..9a19f71c
--- /dev/null
+++ b/lib/Dancer2/FileUtils.pm
@@ -0,0 +1,243 @@
+package Dancer2::FileUtils;
+# ABSTRACT: File utility helpers
+$Dancer2::FileUtils::VERSION = '0.300000';
+use strict;
+use warnings;
+
+use File::Basename ();
+use File::Spec;
+use Carp;
+
+use Exporter 'import';
+our @EXPORT_OK = qw(
+ dirname open_file path read_file_content read_glob_content
+ path_or_empty set_file_mode normalize_path escape_filename
+);
+
+
+sub path {
+ my @parts = @_;
+ my $path = File::Spec->catfile(@parts);
+
+ return normalize_path($path);
+}
+
+sub path_or_empty {
+ my @parts = @_;
+ my $path = path(@parts);
+
+ # return empty if it doesn't exist
+ return -e $path ? $path : '';
+}
+
+sub dirname { File::Basename::dirname(@_) }
+
+sub set_file_mode {
+ my $fh = shift;
+ my $charset = 'utf-8';
+ binmode $fh, ":encoding($charset)";
+ return $fh;
+}
+
+sub open_file {
+ my ( $mode, $filename ) = @_;
+
+ open my $fh, $mode, $filename
+ or croak "Can't open '$filename' using mode '$mode': $!";
+
+ return set_file_mode($fh);
+}
+
+sub read_file_content {
+ my $file = shift or return;
+ my $fh = open_file( '<', $file );
+
+ return wantarray
+ ? read_glob_content($fh)
+ : scalar read_glob_content($fh);
+}
+
+sub read_glob_content {
+ my $fh = shift;
+
+ my @content = <$fh>;
+ close $fh;
+
+ return wantarray ? @content : join '', @content;
+}
+
+sub normalize_path {
+
+ # this is a revised version of what is described in
+ # http://www.linuxjournal.com/content/normalizing-path-names-bash
+ # by Mitch Frazier
+ my $path = shift or return;
+ my $seqregex = qr{
+ [^/]* # anything without a slash
+ /\.\.(/|\z) # that is accompanied by two dots as such
+ }x;
+
+ $path =~ s{/\./}{/}g;
+ while ( $path =~ s{$seqregex}{} ) {}
+
+ #see https://rt.cpan.org/Public/Bug/Display.html?id=80077
+ $path =~ s{^//}{/};
+ return $path;
+}
+
+sub escape_filename {
+ my $filename = shift or return;
+
+ # based on escaping used in CHI::Driver. Our use-case is one-way,
+ # so we allow utf8 chars to be escaped, but NEVER do the inverse
+ # operation.
+ $filename =~ s/([^A-Za-z0-9_\=\-\~])/sprintf("+%02x", ord($1))/ge;
+ return $filename;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::FileUtils - File utility helpers
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ use Dancer2::FileUtils qw/dirname path path_or_empty/;
+
+ # for 'path/to/file'
+ my $dir = dirname($path); # returns 'path/to'
+ my $path = path($path); # returns '/abs/path/to/file'
+ my $path = path_or_empty($path); # returns '' if file doesn't exist
+
+
+ use Dancer2::FileUtils qw/path read_file_content/;
+
+ my $content = read_file_content( path( 'folder', 'folder', 'file' ) );
+ my @content = read_file_content( path( 'folder', 'folder', 'file' ) );
+
+
+ use Dancer2::FileUtils qw/read_glob_content set_file_mode/;
+
+ open my $fh, '<', $file or die "$!\n";
+ set_file_mode($fh);
+ my @content = read_glob_content($fh);
+ my $content = read_glob_content($fh);
+
+
+ use Dancer2::FileUtils qw/open_file/;
+
+ my $fh = open_file('<', $file) or die $message;
+
+
+ use Dancer2::FileUtils 'set_file_mode';
+
+ set_file_mode($fh);
+
+=head1 DESCRIPTION
+
+Dancer2::FileUtils includes a few file related utilities that Dancer2
+uses internally. Developers may use it instead of writing their own
+file reading subroutines or using additional modules.
+
+=head1 FUNCTIONS
+
+=head2 my $path = path( 'folder', 'folder', 'filename');
+
+Provides comfortable path resolution, internally using L<File::Spec>. 'path'
+does not verify paths, it just normalizes the path.
+
+=head2 my $path = path_or_empty('folder, 'folder','filename');
+
+Like path, but returns '' if path doesn't exist.
+
+=head2 dirname
+
+ use Dancer2::FileUtils 'dirname';
+
+ my $dir = dirname($path);
+
+Exposes L<File::Basename>'s I<dirname>, to allow fetching a directory name from
+a path. On most OS, returns all but last level of file path. See
+L<File::Basename> for details.
+
+=head2 set_file_mode($fh);
+
+ use Dancer2::FileUtils 'set_file_mode';
+
+ set_file_mode($fh);
+
+Applies charset setting from Dancer2's configuration. Defaults to utf-8 if no
+charset setting.
+
+=head2 my $fh = open_file('<', $file) or die $message;
+
+ use Dancer2::FileUtils 'open_file';
+ my $fh = open_file('<', $file) or die $message;
+
+Calls open and returns a filehandle. Takes in account the 'charset' setting
+from Dancer2's configuration to open the file in the proper encoding (or
+defaults to utf-8 if setting not present).
+
+=head2 my $content = read_file_content($file);
+
+ use Dancer2::FileUtils 'read_file_content';
+
+ my @content = read_file_content($file);
+ my $content = read_file_content($file);
+
+Returns either the content of a file (whose filename is the input), or I<undef>
+if the file could not be opened.
+
+In array context it returns each line (as defined by $/) as a separate element;
+in scalar context returns the entire contents of the file.
+
+=head2 my $content = read_glob_content($fh);
+
+ use Dancer2::FileUtils 'read_glob_content';
+
+ open my $fh, '<', $file or die "$!\n";
+ binmode $fh, ':encoding(utf-8)';
+ my @content = read_glob_content($fh);
+ my $content = read_glob_content($fh);
+
+Similar to I<read_file_content>, only it accepts a file handle. It is
+assumed that the appropriate PerlIO layers are applied to the file handle.
+Returns the content and B<closes the file handle>.
+
+=head2 my $norm_path=normalize_path ($path);
+
+=head2 my $escaped_filename = escape_filename( $filename );
+
+Escapes characters in a filename that may alter a path when concatenated.
+
+ use Dancer2::FileUtils 'escape_filename';
+
+ my $safe = escape_filename( "a/../b.txt" ); # a+2f+2e+2e+2fb+2etxt
+
+=head1 EXPORT
+
+Nothing by default. You can provide a list of subroutines to import.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Handler/AutoPage.pm b/lib/Dancer2/Handler/AutoPage.pm
new file mode 100644
index 00000000..220926e0
--- /dev/null
+++ b/lib/Dancer2/Handler/AutoPage.pm
@@ -0,0 +1,127 @@
+package Dancer2::Handler::AutoPage;
+# ABSTRACT: Class for handling the AutoPage feature
+$Dancer2::Handler::AutoPage::VERSION = '0.300000';
+use Moo;
+use Carp 'croak';
+use Dancer2::Core::Types;
+
+with qw<
+ Dancer2::Core::Role::Handler
+ Dancer2::Core::Role::StandardResponses
+>;
+
+sub register {
+ my ( $self, $app ) = @_;
+
+ return unless $app->config->{auto_page};
+
+ $app->add_route(
+ method => $_,
+ regexp => $self->regexp,
+ code => $self->code,
+ ) for $self->methods;
+}
+
+sub code {
+ sub {
+ my $app = shift;
+ my $prefix = shift;
+
+ my $template = $app->template_engine;
+ if ( !defined $template ) {
+ $app->response->has_passed(1);
+ return;
+ }
+
+ my $page = $app->request->path;
+ my $layout_dir = $template->layout_dir;
+ if ( $page =~ m{^/\Q$layout_dir\E/} ) {
+ $app->response->has_passed(1);
+ return;
+ }
+
+ # remove leading '/', ensuring paths relative to the view
+ $page =~ s{^/}{};
+ my $view_path = $template->view_pathname($page);
+
+ if ( ! $template->pathname_exists( $view_path ) ) {
+ $app->response->has_passed(1);
+ return;
+ }
+
+ my $ct = $template->process( $page );
+ return ( $app->request->method eq 'GET' ) ? $ct : '';
+ };
+}
+
+sub regexp {'/**'}
+
+sub methods {qw(head get)}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Handler::AutoPage - Class for handling the AutoPage feature
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+The AutoPage feature is a Handler (turned off by default) that is
+responsible for serving pages that match an existing template. If a
+view exists with a name that matches the requested path, Dancer2
+processes the request using the Autopage handler.
+
+To turn it add to your config file:
+
+ auto_page: 1
+
+This allows you to easily serve simple pages without having to write a
+route definition for them.
+
+If there's no view with the name request, the route passes, allowing
+other matching routes to be dispatched.
+
+=head1 METHODS
+
+=head2 register
+
+Creates the routes.
+
+=head2 code
+
+A code reference that processes the route request.
+
+=head2 methods
+
+The methods that should be served for autopages.
+
+Default: B<head>, B<get>.
+
+=head2 regexp
+
+The regexp (path) we want to match.
+
+Default: B</:page>.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Handler/File.pm b/lib/Dancer2/Handler/File.pm
new file mode 100644
index 00000000..2dd3bbcb
--- /dev/null
+++ b/lib/Dancer2/Handler/File.pm
@@ -0,0 +1,179 @@
+package Dancer2::Handler::File;
+# ABSTRACT: class for handling file content rendering
+$Dancer2::Handler::File::VERSION = '0.300000';
+use Carp 'croak';
+use Moo;
+use HTTP::Date;
+use Dancer2::FileUtils 'path', 'open_file', 'read_glob_content';
+use Dancer2::Core::MIME;
+use Dancer2::Core::Types;
+use File::Spec;
+
+with qw<
+ Dancer2::Core::Role::Handler
+ Dancer2::Core::Role::StandardResponses
+ Dancer2::Core::Role::Hookable
+>;
+
+sub hook_aliases {
+ {
+ before_file_render => 'handler.file.before_render',
+ after_file_render => 'handler.file.after_render',
+ }
+}
+
+sub supported_hooks { values %{ shift->hook_aliases } }
+
+has mime => (
+ is => 'ro',
+ isa => InstanceOf ['Dancer2::Core::MIME'],
+ default => sub { Dancer2::Core::MIME->new },
+);
+
+has encoding => (
+ is => 'ro',
+ default => sub {'utf-8'},
+);
+
+has public_dir => (
+ is => 'ro',
+ lazy => 1,
+ builder => '_build_public_dir',
+);
+
+has regexp => (
+ is => 'ro',
+ default => sub {'/**'},
+);
+
+sub _build_public_dir {
+ my $self = shift;
+ return $self->app->config->{public_dir}
+ || $ENV{DANCER_PUBLIC}
+ || path( $self->app->location, 'public' );
+}
+
+sub register {
+ my ( $self, $app ) = @_;
+
+ # don't register the handler if no valid public dir
+ return if !-d $self->public_dir;
+
+ $app->add_route(
+ method => $_,
+ regexp => $self->regexp,
+ code => $self->code( $app->prefix ),
+ ) for $self->methods;
+}
+
+sub methods { ( 'head', 'get' ) }
+
+sub code {
+ my ( $self, $prefix ) = @_;
+
+ sub {
+ my $app = shift;
+ my $prefix = shift;
+ my $path = $app->request->path_info;
+
+ if ( $path =~ /\0/ ) {
+ return $self->standard_response( $app, 400 );
+ }
+
+ if ( $prefix && $prefix ne '/' ) {
+ $path =~ s/^\Q$prefix\E//;
+ }
+
+ my $file_path = $self->merge_paths( $path, $self->public_dir );
+ return $self->standard_response( $app, 403 ) if !defined $file_path;
+
+ if ( !-f $file_path ) {
+ $app->response->has_passed(1);
+ return;
+ }
+
+ if ( !-r $file_path ) {
+ return $self->standard_response( $app, 403 );
+ }
+
+ # Now we are sure we can render the file...
+ $self->execute_hook( 'handler.file.before_render', $file_path );
+
+ # Read file content as bytes
+ my $fh = open_file( "<", $file_path );
+ binmode $fh;
+ my $content = read_glob_content($fh);
+
+ # Assume m/^text/ mime types are correctly encoded
+ my $content_type = $self->mime->for_file($file_path) || 'text/plain';
+ if ( $content_type =~ m!^text/! ) {
+ $content_type .= "; charset=" . ( $self->encoding || "utf-8" );
+ }
+
+ my @stat = stat $file_path;
+
+ $app->response->header('Content-Type')
+ or $app->response->header( 'Content-Type', $content_type );
+
+ $app->response->header('Content-Length')
+ or $app->response->header( 'Content-Length', $stat[7] );
+
+ $app->response->header('Last-Modified')
+ or $app->response->header(
+ 'Last-Modified',
+ HTTP::Date::time2str( $stat[9] )
+ );
+
+ $app->response->content($content);
+ $app->response->is_encoded(1); # bytes are already encoded
+ $self->execute_hook( 'handler.file.after_render', $app->response );
+ return ( $app->request->method eq 'GET' ) ? $content : '';
+ };
+}
+
+sub merge_paths {
+ my ( undef, $path, $public_dir ) = @_;
+
+ my ( $volume, $dirs, $file ) = File::Spec->splitpath( $path );
+ my @tokens = File::Spec->splitdir( "$dirs$file" );
+ my $updir = File::Spec->updir;
+ return if grep $_ eq $updir, @tokens;
+
+ my ( $pub_vol, $pub_dirs, $pub_file ) = File::Spec->splitpath( $public_dir );
+ my @pub_tokens = File::Spec->splitdir( "$pub_dirs$pub_file" );
+ return if length $volume and length $pub_vol and $volume ne $pub_vol;
+
+ my @final_vol = ( length $pub_vol ? $pub_vol : length $volume ? $volume : () );
+ my @file_path = ( @final_vol, @pub_tokens, @tokens );
+ my $file_path = path( @file_path );
+ return $file_path;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Handler::File - class for handling file content rendering
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Logger/Capture.pm b/lib/Dancer2/Logger/Capture.pm
new file mode 100644
index 00000000..87c09c3b
--- /dev/null
+++ b/lib/Dancer2/Logger/Capture.pm
@@ -0,0 +1,141 @@
+package Dancer2::Logger::Capture;
+# ABSTRACT: Capture dancer logs
+$Dancer2::Logger::Capture::VERSION = '0.300000';
+use Moo;
+use Dancer2::Logger::Capture::Trap;
+
+with 'Dancer2::Core::Role::Logger';
+
+has trapper => (
+ is => 'ro',
+ lazy => 1,
+ builder => '_build_trapper',
+);
+
+sub _build_trapper { Dancer2::Logger::Capture::Trap->new }
+
+sub log {
+ my ( $self, $level, $message ) = @_;
+
+ $self->trapper->store(
+ $level, $message, $self->format_message( $level => $message )
+ );
+
+ return;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Logger::Capture - Capture dancer logs
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+The basics:
+
+ set logger => "capture";
+
+ my $trap = dancer_app->logger_engine->trapper;
+ my $logs = $trap->read;
+
+A worked-out real-world example:
+
+ use Test::More tests => 2;
+ use Dancer2;
+
+ set logger => 'capture';
+
+ warning "Danger! Warning!";
+ debug "I like pie.";
+
+ my $trap = dancer_app->logger_engine->trapper;
+
+ is_deeply $trap->read, [
+ { level => "warning", message => "Danger! Warning!" },
+ { level => "debug", message => "I like pie.", }
+ ];
+
+ # each call to read cleans the trap
+ is_deeply $trap->read, [];
+
+=head1 DESCRIPTION
+
+This is a logger class for L<Dancer2> which captures all logs to an object.
+
+It's primary purpose is for testing. Here is an example of a test:
+
+ use strict;
+ use warnings;
+ use Test::More;
+ use Plack::Test;
+ use HTTP::Request::Common;
+ use Ref::Util qw<is_coderef>;
+
+ {
+ package App;
+ use Dancer2;
+
+ set log => 'debug';
+ set logger => 'capture';
+
+ get '/' => sub {
+ log(debug => 'this is my debug message');
+ log(core => 'this should not be logged');
+ log(info => 'this is my info message');
+ };
+ }
+
+ my $app = Dancer2->psgi_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ my $res = $cb->( GET '/' );
+
+ my $trap = App->dancer_app->logger_engine->trapper;
+
+ is_deeply $trap->read, [
+ { level => 'debug', message => 'this is my debug message' },
+ { level => 'info', message => 'this is my info message' },
+ ];
+
+ is_deeply $trap->read, [];
+ };
+
+ done_testing;
+
+=head1 METHODS
+
+=head2 trapper
+
+Returns the L<Dancer2::Logger::Capture::Trap> object used to capture
+and read logs.
+
+=head1 SEE ALSO
+
+L<Dancer2::Core::Role::Logger>, L<Dancer2::Logger::Capture::Trap>
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Logger/Capture/Trap.pm b/lib/Dancer2/Logger/Capture/Trap.pm
new file mode 100644
index 00000000..df9eaf80
--- /dev/null
+++ b/lib/Dancer2/Logger/Capture/Trap.pm
@@ -0,0 +1,94 @@
+package Dancer2::Logger::Capture::Trap;
+# ABSTRACT: a place to store captured Dancer2 logs
+$Dancer2::Logger::Capture::Trap::VERSION = '0.300000';
+use Moo;
+use Dancer2::Core::Types;
+
+has storage => (
+ is => 'rw',
+ isa => ArrayRef,
+ default => sub { [] },
+);
+
+sub store {
+ my ( $self, $level, $message, $fmt_string ) = @_;
+ push @{ $self->storage }, {
+ level => $level,
+ message => $message,
+ formatted => $fmt_string,
+ };
+}
+
+sub read {
+ my $self = shift;
+
+ my $logs = $self->storage;
+ $self->storage( [] );
+ return $logs;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Logger::Capture::Trap - a place to store captured Dancer2 logs
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ my $trap = Dancer2::Logger::Capture::Trap->new;
+ $trap->store( $level, $message );
+ my $logs = $trap->read;
+
+=head1 DESCRIPTION
+
+This is a place to store and retrieve capture Dancer2 logs used by
+L<Dancer2::Logger::Capture>.
+
+=head2 Methods
+
+=head3 new
+
+=head3 store
+
+ $trap->store($level, $message);
+
+Stores a log $message and its $level.
+
+=head3 read
+
+ my $logs = $trap->read;
+
+Returns the logs stored as an array ref and clears the storage.
+
+For example...
+
+ [{ level => "warning", message => "Danger! Warning! Dancer2!" },
+ { level => "error", message => "You fail forever" }
+ ];
+
+=head1 SEE ALSO
+
+L<Dancer2::Logger::Capture>
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Logger/Console.pm b/lib/Dancer2/Logger/Console.pm
new file mode 100644
index 00000000..9ec24fdb
--- /dev/null
+++ b/lib/Dancer2/Logger/Console.pm
@@ -0,0 +1,62 @@
+package Dancer2::Logger::Console;
+# ABSTRACT: Console logger
+$Dancer2::Logger::Console::VERSION = '0.300000';
+use Moo;
+
+with 'Dancer2::Core::Role::Logger';
+
+sub log {
+ my ( $self, $level, $message ) = @_;
+ print STDERR $self->format_message( $level => $message );
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Logger::Console - Console logger
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This is a logging engine that allows you to print debug messages on the
+standard error output.
+
+=head1 METHODS
+
+=head2 log
+
+Writes the log message to the console.
+
+=head1 CONFIGURATION
+
+The setting C<logger> should be set to C<console> in order to use this logging
+engine in a Dancer2 application.
+
+There is no additional setting available with this engine.
+
+=head1 SEE ALSO
+
+L<Dancer2::Core::Role::Logger>
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Logger/Diag.pm b/lib/Dancer2/Logger/Diag.pm
new file mode 100644
index 00000000..be27b283
--- /dev/null
+++ b/lib/Dancer2/Logger/Diag.pm
@@ -0,0 +1,55 @@
+package Dancer2::Logger::Diag;
+# ABSTRACT: Test::More diag() logging engine for Dancer2
+$Dancer2::Logger::Diag::VERSION = '0.300000';
+use Moo;
+use Test::More;
+
+with 'Dancer2::Core::Role::Logger';
+
+sub log {
+ my ( $self, $level, $message ) = @_;
+
+ Test::More::diag( $self->format_message( $level => $message ) );
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Logger::Diag - Test::More diag() logging engine for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This logging engine uses L<Test::More>'s diag() to output as TAP comments.
+
+This is very useful in case you're writing a test and want to have logging
+messages as part of your TAP.
+
+=head1 METHODS
+
+=head2 log
+
+Use Test::More's diag() to output the log message.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Logger/File.pm b/lib/Dancer2/Logger/File.pm
new file mode 100644
index 00000000..29b5790d
--- /dev/null
+++ b/lib/Dancer2/Logger/File.pm
@@ -0,0 +1,173 @@
+package Dancer2::Logger::File;
+# ABSTRACT: file-based logging engine for Dancer2
+$Dancer2::Logger::File::VERSION = '0.300000';
+use Carp 'carp';
+use Moo;
+use Dancer2::Core::Types;
+
+with 'Dancer2::Core::Role::Logger';
+
+use File::Spec;
+use Fcntl qw(:flock SEEK_END);
+use Dancer2::FileUtils qw(open_file);
+use IO::File;
+
+has environment => (
+ is => 'ro',
+ required => 1,
+);
+
+has location => (
+ is => 'ro',
+ required => 1,
+);
+
+has log_dir => (
+ is => 'rw',
+ isa => sub {
+ my $dir = shift;
+
+ if ( !-d $dir && !mkdir $dir ) {
+ die "log directory \"$dir\" does not exist and unable to create it.";
+ }
+ if ( !-w $dir ) {
+ die "log directory \"$dir\" is not writable."
+ }
+ },
+ lazy => 1,
+ builder => '_build_log_dir',
+);
+
+has file_name => (
+ is => 'ro',
+ isa => Str,
+ builder => '_build_file_name',
+ lazy => 1
+);
+
+has log_file => (
+ is => 'ro',
+ isa => Str,
+ lazy => 1,
+ builder => '_build_log_file',
+);
+
+has fh => (
+ is => 'ro',
+ lazy => 1,
+ builder => '_build_fh',
+);
+
+sub _build_log_dir { File::Spec->catdir( $_[0]->location, 'logs' ) }
+
+sub _build_file_name {$_[0]->environment . ".log"}
+
+sub _build_log_file {
+ my $self = shift;
+ return File::Spec->catfile( $self->log_dir, $self->file_name );
+}
+
+sub _build_fh {
+ my $self = shift;
+ my $logfile = $self->log_file;
+
+ my $fh;
+ unless ( $fh = open_file( '>>', $logfile ) ) {
+ carp "unable to create or append to $logfile";
+ return;
+ }
+
+ $fh->autoflush;
+
+ return $fh;
+}
+
+sub log {
+ my ( $self, $level, $message ) = @_;
+ my $fh = $self->fh;
+
+ return unless ( ref $fh && $fh->opened );
+
+ flock( $fh, LOCK_EX )
+ or carp "locking logfile $self->{logfile} failed: $!";
+ seek( $fh, 0, SEEK_END );
+ $fh->print( $self->format_message( $level => $message ) )
+ or carp "writing to logfile $self->{logfile} failed";
+ flock( $fh, LOCK_UN )
+ or carp "unlocking logfile $self->{logfile} failed: $!";
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Logger::File - file-based logging engine for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This is a logging engine that allows you to save your logs to files on disk.
+
+Logs are not automatically rotated. Use a log rotation tool like
+C<logrotate> in C<copytruncate> mode.
+
+=head1 METHODS
+
+=head2 log($level, $message)
+
+Writes the log message to the file.
+
+=head1 CONFIGURATION
+
+The setting C<logger> should be set to C<File> in order to use this logging
+engine in a Dancer2 application.
+
+The follow attributes are supported:
+
+=over 4
+
+=item * C<log_dir>
+
+Directory path to hold log files.
+
+Defaults to F<logs> in the application directory
+
+=item * C<file_name>
+
+The name of the log file.
+
+Defaults to the environment name with a F<.log> suffix
+
+=back
+
+Here is an example configuration that use this logger and stores logs in F</var/log/myapp>:
+
+ logger: "File"
+
+ engines:
+ logger:
+ File:
+ log_dir: "/var/log/myapp"
+ file_name: "myapp.log"
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Logger/Note.pm b/lib/Dancer2/Logger/Note.pm
new file mode 100644
index 00000000..fa58a862
--- /dev/null
+++ b/lib/Dancer2/Logger/Note.pm
@@ -0,0 +1,58 @@
+package Dancer2::Logger::Note;
+# ABSTRACT: Test::More note() logging engine for Dancer2
+$Dancer2::Logger::Note::VERSION = '0.300000';
+use Moo;
+use Test::More;
+
+with 'Dancer2::Core::Role::Logger';
+
+sub log {
+ my ( $self, $level, $message ) = @_;
+
+ Test::More::note( $self->format_message( $level => $message ) );
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Logger::Note - Test::More note() logging engine for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This logging engine uses L<Test::More>'s note() to output as TAP comments.
+
+This is very useful in case you're writing a test and want to have logging
+messages as part of your TAP.
+
+"Like C<diag()>, except the message will not be seen when the test is run in a
+harness. It will only be visible in the verbose TAP stream." -- Test::More.
+
+=head1 METHODS
+
+=head2 log
+
+Use Test::More's note() to output the log message.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Logger/Null.pm b/lib/Dancer2/Logger/Null.pm
new file mode 100644
index 00000000..62d68035
--- /dev/null
+++ b/lib/Dancer2/Logger/Null.pm
@@ -0,0 +1,47 @@
+package Dancer2::Logger::Null;
+# ABSTRACT: Blackhole-like silent logging engine for Dancer2
+$Dancer2::Logger::Null::VERSION = '0.300000';
+use Moo;
+with 'Dancer2::Core::Role::Logger';
+
+sub log {1}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Logger::Null - Blackhole-like silent logging engine for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This logger acts as a blackhole (or /dev/null, if you will) that discards all
+the log messages instead of displaying them anywhere.
+
+=head1 METHODS
+
+=head2 log
+
+Discards the message.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Manual.pod b/lib/Dancer2/Manual.pod
new file mode 100644
index 00000000..cf35d680
--- /dev/null
+++ b/lib/Dancer2/Manual.pod
@@ -0,0 +1,3368 @@
+# ABSTRACT: A gentle introduction to Dancer2
+package Dancer2::Manual;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Manual - A gentle introduction to Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Dancer2 is a free and open source web application framework written in Perl.
+
+It's a complete rewrite of L<Dancer>, based on L<Moo> and using a more
+robust and extensible fully-OO design.
+
+It's designed to be powerful and flexible, but also easy to use - getting up
+and running with your web app is trivial, and an ecosystem of adaptors for
+common template engines, session storage, logging methods, serializers, and
+plugins to make common tasks easy means you can do what you want to do, your
+way, easily.
+
+=head1 INSTALL
+
+Installation of Dancer2 is simple, using your favourite method to install from
+CPAN, e.g.:
+
+ perl -MCPAN -e 'install Dancer2'
+
+Thanks to the magic of cpanminus, if you do not have CPAN.pm configured, or
+just want a quickfire way to get running, the following should work, at
+least on Unix-like systems:
+
+ wget -O - http://cpanmin.us | sudo perl - Dancer2
+
+(If you don't have root access, omit the 'sudo', and cpanminus will install
+Dancer2 and prereqs into C<~/perl5>.)
+
+Dancer2 is also available as a package from the package repository of several
+distributions, for example on Debian/Ubuntu you should be able to just:
+
+ apt-get install libdancer2-perl
+
+Do be aware, though, that distribution-packaged versions sometimes lag behind
+the most recent version on CPAN.
+
+=head1 BOOTSTRAPPING A NEW APP
+
+Create a web application using the dancer script:
+
+ $ dancer2 -a MyApp && cd MyApp
+ + MyApp
+ + MyApp/config.yml
+ + MyApp/Makefile.PL
+ + MyApp/MANIFEST.SKIP
+ + MyApp/.dancer
+ + MyApp/cpanfile
+ + MyApp/bin
+ + MyApp/bin/app.psgi
+ + MyApp/environments
+ + MyApp/environments/development.yml
+ + MyApp/environments/production.yml
+ + MyApp/lib
+ + MyApp/lib/MyApp.pm
+ + MyApp/public
+ + MyApp/public/favicon.ico
+ + MyApp/public/500.html
+ + MyApp/public/dispatch.cgi
+ + MyApp/public/404.html
+ + MyApp/public/dispatch.fcgi
+ + MyApp/public/css
+ + MyApp/public/css/error.css
+ + MyApp/public/css/style.css
+ + MyApp/public/images
+ + MyApp/public/images/perldancer.jpg
+ + MyApp/public/images/perldancer-bg.jpg
+ + MyApp/public/javascripts
+ + MyApp/public/javascripts/jquery.js
+ + MyApp/t
+ + MyApp/t/001_base.t
+ + MyApp/t/002_index_route.t
+ + MyApp/views
+ + MyApp/views/index.tt
+ + MyApp/views/layouts
+ + MyApp/views/layouts/main.tt
+
+It creates a directory named after the name of the app, along with a
+configuration file, a views directory (where your templates and layouts
+will live), an environments directory (where environment-specific
+settings live), a module containing the actual guts of your application, and
+a script to start it. A default skeleton is used to bootstrap the new
+application, but you can use the C<-s> option to provide another skeleton.
+For example:
+
+ $ dancer2 -a MyApp -s ~/mydancerskel
+
+For an example of a skeleton directory check the default one available in
+the C<share/> directory of your Dancer2 distribution.
+
+(In what follows we will refer to the directory in which you have created your
+Dancer2 application -- I<e.g.,> what C<MyApp> was above -- as the
+C<appdir>.)
+
+Because Dancer2 is a L<PSGI> web application framework, you can use the
+C<plackup> tool (provided by L<Plack>) for launching the application:
+
+ plackup -p 5000 bin/app.psgi
+
+View the web application at:
+
+ http://localhost:5000
+
+=head1 USAGE
+
+When Dancer2 is imported to a script, that script becomes a webapp, and at
+this point, all the script has to do is declare a list of B<routes>. A
+route handler is composed by an HTTP method, a path pattern and a code
+block. C<strict>, C<warnings> and C<utf8> pragmas are also imported with
+Dancer2.
+
+The code block given to the route handler has to return a string which will
+be used as the content to render to the client.
+
+Routes are defined for a given HTTP method. For each method supported, a
+keyword is exported by the module.
+
+=head2 HTTP Methods
+
+Here are some of the standard HTTP methods which you can use to define your
+route handlers.
+
+=over 4
+
+=item * B<GET> The GET method retrieves information, and is the most common
+
+GET requests should be used for typical "fetch" requests - retrieving
+information. They should not be used for requests which change data on the
+server or have other effects.
+
+When defining a route handler for the GET method, Dancer2 automatically
+defines a route handler for the HEAD method (in order to honour HEAD
+requests for each of your GET route handlers).
+
+To define a GET action, use the L<get|Dancer2::Manual/get> keyword.
+
+=item * B<POST> The POST method is used to create a resource on the server.
+
+To define a POST action, use the L<post|Dancer2::Manual/post> keyword.
+
+=item * B<PUT> The PUT method is used to replace an existing resource.
+
+To define a PUT action, use the L<put|Dancer2::Manual/put> keyword.
+
+a PUT request should replace the existing resource with that specified - for
+instance - if you wanted to just update an email address for a user, you'd
+have to specify all attributes of the user again; to make a partial update,
+a PATCH request is used.
+
+=item * B<PATCH> The PATCH method updates some attributes of an existing resource.
+
+To define a PATCH action, use the L<patch|Dancer2::Manual/patch> keyword.
+
+=item * B<DELETE> The DELETE method requests that the origin server delete the
+resource identified by the Request-URI.
+
+To define a DELETE action, use the L<del|Dancer2::Manual/del> keyword.
+
+=back
+
+=head3 Handling multiple HTTP request methods
+
+Routes can use C<any> to match all, or a specified list of HTTP methods.
+
+The following will match any HTTP request to the path C</myaction>:
+
+ any '/myaction' => sub {
+ # code
+ }
+
+The following will match GET or POST requests to C</myaction>:
+
+ any ['get', 'post'] => '/myaction' => sub {
+ # code
+ };
+
+For convenience, any route which matches GET requests will also match HEAD
+requests.
+
+=head2 Route Handlers
+
+The route action is the code reference declared. It can access parameters
+through the specific L<route_parameters|/route_parameters>,
+L<query_parameters|/query_parameters>, and L<body_parameters|/body_parameters>
+keywords, which return a L<Hash::MultiValue> object.
+This hashref is a merge of the route pattern matches and the request params.
+
+You can find more details about how params are built and how to access them
+in the L<Dancer2::Core::Request> documentation.
+
+=head3 Declaring Routes
+
+To control what happens when a web request is received by your webapp,
+you'll need to declare C<routes>. A route declaration indicates which HTTP
+method(s) it is valid for, the path it matches (e.g. C</foo/bar>), and a
+coderef to execute, which returns the response.
+
+ get '/hello/:name' => sub {
+ return "Hi there " . route_parameters->get('name');
+ };
+
+The above route specifies that, for GET requests to C</hello/...>, the code
+block provided should be executed.
+
+=head3 Retrieving request parameters
+
+The L<query_parameters|Dancer2::Manual/query_parameters>,
+L<route_parameters|Dancer2::Manual/route_parameters>, and
+L<body_parameters|Dancer2::Manual/body_parameters> keywords provide
+a L<Hash::MultiValue> result from the three different parameters.
+
+=head3 Named matching
+
+A route pattern can contain one or more tokens (a word prefixed with ':').
+Each token found in a route pattern is used as a named-pattern match. Any
+match will be set in the route parameters.
+
+ get '/hello/:name' => sub {
+ "Hey " . route_parameters->get('name') . ", welcome here!";
+ };
+
+Tokens can be optional, for example:
+
+ get '/hello/:name?' => sub {
+ my $name = route_parameters->get('name') //= 'Whoever you are';
+ "Hello there, $name";
+ };
+
+=head3 Named matching with type constraints
+
+Type constraints can be added to tokens.
+
+ get '/user/:id[Int]' => sub {
+ # matches /user/34 but not /user/jamesdean
+ my $user_id = route_parameters->get('id');
+ };
+
+ get '/user/:username[Str]' => sub {
+ # matches /user/jamesdean but not /user/34 since that is caught
+ # by previous route
+ my $username = route_parameters->get('username');
+ };
+
+You can even use type constraints to add a regexp check:
+
+ get '/book/:date[StrMatch[qr{\d\d\d\d-\d\d-\d\d}]]' => sub {
+ # matches /book/2014-02-04
+ my $date = route_parameters->get('date');
+ };
+
+The default type library is L<Dancer2::Core::Types> but any type library
+built using L<Type::Tiny>'s L<Type::Library> can be used instead.
+If you'd like to use a different default type library you must declare it
+in the configuration file, for example:
+
+ type_library: My::Type::Library
+
+Alternatively you can specify the type library in which the type is defined
+as part of the route definition:
+
+ get '/user/:username[My::Type::Library::Username]' => sub {
+ my $username = route_parameters->get('username');
+ };
+
+This will load C<My::Type::Library> and from it use the type C<Username>. This
+allows types to be used that are not part of the type library defined by config's
+C<type_library>.
+
+More complex constructs are allowed such as:
+
+ get '/some/:thing[Int|MyDate]' => sub {
+ ...;
+ };
+
+See L<Type::Registry/lookup($name)> for more details.
+
+=head3 Wildcard Matching
+
+A route can contain a wildcard (represented by a C<*>). Each wildcard match
+will be placed in a list, which the C<splat> keyword returns.
+
+ get '/download/*.*' => sub {
+ my ($file, $ext) = splat;
+ # do something with $file.$ext here
+ };
+
+An extensive, greedier wildcard represented by C<**> (A.K.A. "megasplat") can be
+used to define a route. The additional path is broken down and returned as an
+arrayref:
+
+ get '/entry/*/tags/**' => sub {
+ my ( $entry_id, $tags ) = splat;
+ my @tags = @{$tags};
+ };
+
+The C<splat> keyword in the above example for the route F</entry/1/tags/one/two>
+would set C<$entry_id> to C<1> and C<$tags> to C<['one', 'two']>.
+
+=head3 Mixed named and wildcard matching
+
+A route can combine named (token) matching and wildcard matching.
+This is useful when chaining actions:
+
+ get '/team/:team/**' => sub {
+ var team => route_parameters->get('team');
+ pass;
+ };
+
+ prefix '/team/:team';
+
+ get '/player/*' => sub {
+ my ($player) = splat;
+
+ # etc...
+ };
+
+ get '/score' => sub {
+ return score_for( vars->{'team'} );
+ };
+
+=head3 Regular Expression Matching
+
+A route can be defined with a Perl regular expression.
+
+In order to tell Dancer2 to consider the route as a real regexp, the route
+must be defined explicitly with C<qr{}>, like the following:
+
+ get qr{/hello/([\w]+)} => sub {
+ my ($name) = splat;
+ return "Hello $name";
+ };
+
+For Perl 5.10+, a route regex may use named capture groups. The C<captures>
+keyword will return a reference to a copy of C<%+>.
+
+=head3 Conditional Matching
+
+Routes may include some matching conditions (on content_type, agent,
+user_agent, content_length and path_info):
+
+ get '/foo', {agent => 'Songbird (\d\.\d)[\d\/]*?'} => sub {
+ 'foo method for songbird'
+ }
+
+ get '/foo' => sub {
+ 'all browsers except songbird'
+ }
+
+=head2 Prefix
+
+A prefix can be defined for each route handler, like this:
+
+ prefix '/home';
+
+From here, any route handler is defined to /home/*
+
+ get '/page1' => sub {}; # will match '/home/page1'
+
+You can unset the prefix value
+
+ prefix '/'; # or: prefix undef;
+ get '/page1' => sub {}; # will match /page1
+
+Alternatively, to prevent you from ever forgetting to undef the prefix, you
+can use lexical prefix like this:
+
+ prefix '/home' => sub {
+ get '/page1' => sub {}; # will match '/home/page1'
+ }; ## prefix reset to previous value on exit
+
+ get '/page1' => sub {}; # will match /page1
+
+=head2 Delayed responses (Async/Streaming)
+
+L<Dancer2> can provide delayed (otherwise known as I<asynchronous>) responses
+using the C<delayed> keyword. These responses are streamed, although you can
+set the content all at once, if you prefer.
+
+ get '/status' => sub {
+ delayed {
+ response_header 'X-Foo' => 'Bar';
+
+ # flush headers (in case of streaming)
+ flush;
+
+ # send content to the user
+ content 'Hello, world!';
+
+ # you can write more content
+ # all streaming
+ content 'Hello, again!';
+
+ # when done, close the connection
+ done;
+
+ # do whatever you want else, asynchronously
+ # the user socket closed by now
+ ...
+ };
+ };
+
+If you are streaming (calling C<content> several times), you must call
+C<flush> first. If you're sending only once, you don't need to call C<flush>.
+
+Here is an example of using delayed responses with L<AnyEvent>:
+
+ use Dancer2;
+ use AnyEvent;
+
+ my %timers;
+ my $count = 5;
+ get '/drums' => sub {
+ delayed {
+ print "Stretching...\n";
+ flush; # necessary, since we're streaming
+
+ $timers{'Snare'} = AE::timer 1, 1, delayed {
+ $timers{'HiHat'} ||= AE::timer 0, 0.5, delayed {
+ content "Tss...\n";
+ };
+
+ content "Bap!\n";
+
+ if ( $count-- == 0 ) {
+ %timers = ();
+ content "Tugu tugu tugu dum!\n";
+ done;
+
+ print "<enter sound of applause>\n\n";
+ $timers{'Applause'} = AE::timer 3, 0, sub {
+ # the DSL will not available here
+ # because we didn't call the "delayed" keyword
+ print "<applause dies out>\n";
+ };
+ }
+ };
+ };
+ };
+
+If an error happens during a write operation, a warning will be issued
+to the logger.
+
+You can handle the error yourself by providing an C<on_error> handler:
+
+ get '/' => sub {
+ delayed {
+ flush;
+ content "works";
+
+ # ... user disconnected here ...
+
+ content "fails";
+
+ # ... error triggered ...
+
+ done; # doesn't even get run
+ } on_error => sub {
+ # delayed{} not needed, DSL already available
+ my ($error) = @_;
+ # do something with $error
+ };
+ };
+
+Here is an example that asynchronously streams the contents of a CSV file:
+
+ use Dancer2;
+ use Text::CSV_XS qw< csv >;
+ use Path::Tiny qw< path >;
+ use JSON::MaybeXS qw< encode_json >;
+ # Create CSV parser
+ my $csv = Text::CSV_XS->new({
+ binary => 1,
+ auto_diag => 1,
+ });
+ get '/' => sub {
+ # delayed response:
+ delayed {
+ # streaming content
+ flush;
+ # Read each row and stream it in JSON
+ my $fh = path('filename.csv')->openr_utf8;
+ while ( my $row = $csv->getline($fh) ) {
+ content encode_json $row;
+ }
+ # close user connection
+ done;
+ } on_error => sub {
+ my ($error) = @_;
+ warning 'Failed to stream to user: ' . request->remote_address;
+ };
+ };
+
+B<NOTE:> If you just want to send a file's contents asynchronously,
+use C<send_file($filename)> instead of C<delayed>, as it will
+automatically take advantage of any asynchronous capability.
+
+=head2 Action Skipping
+
+An action can choose not to serve the current request and ask Dancer2 to
+process the request with the next matching route.
+
+This is done with the B<pass> keyword, like in the following example
+
+ get '/say/:word' => sub {
+ pass if route_parameters->get('word') =~ /^\d+$/;
+ "I say a word: " . route_parameters->get('word');
+ };
+
+ get '/say/:number' => sub {
+ "I say a number: " . route_parameters->get('number');
+ };
+
+=head1 HOOKS
+
+Hooks are code references (or anonymous subroutines) that are triggered at
+specific moments during the resolution of a request. They are set up using the
+L<hook|/hook> keyword.
+
+Many of them are provided by Dancer2's core, but plugins and engines can also
+define their own.
+
+=over 4
+
+=item * C<before> hooks
+
+C<before> hooks are evaluated before each request within the context of the
+request and receives as argument the app (a L<Dancer2::Core::App> object).
+
+It's possible to define variables which will be accessible in the action
+blocks with the L<var keyword|/var>.
+
+ hook before => sub {
+ var note => 'Hi there';
+ };
+
+ get '/foo/*' => sub {
+ my ($match) = splat; # 'oversee';
+ vars->{note}; # 'Hi there'
+ };
+
+For another example, this can be used along with session support to easily
+give non-logged-in users a login page:
+
+ hook before => sub {
+ if (!session('user') && request->path !~ m{^/login}) {
+ # Pass the original path requested along to the handler:
+ forward '/login', { requested_path => request->path };
+ }
+ };
+
+The request keyword returns the current L<Dancer2::Core::Request> object
+representing the incoming request.
+
+=item * C<after> hooks
+
+C<after> hooks are evaluated after the response has been built by a route
+handler, and can alter the response itself, just before it's sent to the
+client.
+
+This hook runs after a request has been processed, but before the response
+is sent.
+
+It receives a L<Dancer2::Core::Response> object, which it can modify if it
+needs to make changes to the response which is about to be sent.
+
+The hook can use other keywords in order to do whatever it wants.
+
+ hook after => sub {
+ response->content(
+ q{The "after" hook can alter the response's content here!}
+ );
+ };
+
+=back
+
+=head2 Templates
+
+=over 4
+
+=item * C<before_template_render>
+
+C<before_template_render> hooks are called whenever a template is going to
+be processed, they are passed the tokens hash which they can alter.
+
+ hook before_template_render => sub {
+ my $tokens = shift;
+ $tokens->{foo} = 'bar';
+ };
+
+The tokens hash will then be passed to the template with all the
+modifications performed by the hook. This is a good way to setup some
+global vars you like to have in all your templates, like the name of the
+user logged in or a section name.
+
+=item * C<after_template_render>
+
+C<after_template_render> hooks are called after the view has been rendered.
+They receive as their first argument the reference to the content that has
+been produced. This can be used to post-process the content rendered by the
+template engine.
+
+ hook after_template_render => sub {
+ my $ref_content = shift;
+ my $content = ${$ref_content};
+
+ # do something with $content
+ ${$ref_content} = $content;
+ };
+
+=item * C<before_layout_render>
+
+C<before_layout_render> hooks are called whenever the layout is going to be
+applied to the current content. The arguments received by the hook are the
+current tokens hashref and a reference to the current content.
+
+ hook before_layout_render => sub {
+ my ($tokens, $ref_content) = @_;
+ $tokens->{new_stuff} = 42;
+ $ref_content = \"new content";
+ };
+
+=item * C<after_layout_render>
+
+C<after_layout_render> hooks are called once the complete content of the
+view has been produced, after the layout has been applied to the content.
+The argument received by the hook is a reference to the complete content
+string.
+
+ hook after_layout_render => sub {
+ my $ref_content = shift;
+ # do something with ${ $ref_content }, which reflects directly
+ # in the caller
+ };
+
+=back
+
+=head2 Error Handling
+
+Refer to L<Error Hooks|Dancer2::Manual/Error-Hooks>
+for details about the following hooks:
+
+=over 4
+
+=item * C<init_error>
+
+=item * C<before_error>
+
+=item * C<after_error>
+
+=item * C<on_route_exception>
+
+=back
+
+=head2 File Rendering
+
+Refer to L<File Handler|Dancer2::Manual/File-Handler>
+for details on the following hooks:
+
+=over 4
+
+=item * C<before_file_render>
+
+=item * C<after_file_render>
+
+=back
+
+=head2 Serializers
+
+=over 4
+
+=item * C<before_serializer> is called before serializing the content, and receives
+the content to serialize as an argument.
+
+ hook before_serializer => sub {
+ my $content = shift;
+ ...
+ };
+
+=item * C<after_serializer> is called after the payload has been serialized, and
+receives the serialized content as an argument.
+
+ hook after_serializer => sub {
+ my $serialized_content = shift;
+ ...
+ };
+
+=back
+
+=head1 HANDLERS
+
+=head2 File Handler
+
+Whenever a content is produced out of the parsing of a static file, the
+L<Dancer2::Handler::File> component is used. This component provides two
+hooks, C<before_file_render> and C<after_file_render>.
+
+C<before_file_render> hooks are called just before starting to parse the
+file, the hook receives as its first argument the file path that is going to
+be processed.
+
+ hook before_file_render => sub {
+ my $path = shift;
+ };
+
+C<after_file_render> hooks are called after the file has been parsed and the
+response content produced. It receives the response object
+(L<Dancer2::Core::Response>) produced.
+
+ hook after_file_render => sub {
+ my $response = shift;
+ };
+
+=head2 Auto page
+
+Whenever a page that matches an existing template needs to be served, the
+L<Dancer2::Handler::AutoPage> component is used.
+
+=head2 Writing your own
+
+A route handler is a class that consumes the L<Dancer2::Core::Role::Handler>
+role. The class must implement a set of methods: C<methods>, C<regexp> and
+C<code> which will be used to declare the route.
+
+Let's look at L<Dancer2::Handler::AutoPage> for example.
+
+First, the matching methods are C<get> and C<head>:
+
+ sub methods { qw(head get) }
+
+Then, the C<regexp> or the I<path> we want to match:
+
+ sub regexp { '/:page' }
+
+Anything will be matched by this route, since we want to check if there's
+a view named with the value of the C<page> token. If not, the route needs
+to C<pass>, letting the dispatching flow to proceed further.
+
+ sub code {
+ sub {
+ my $app = shift;
+ my $prefix = shift;
+
+ my $template = $app->template_engine;
+ if ( !defined $template ) {
+ $app->response->has_passed(1);
+ return;
+ }
+
+ my $page = $app->request->path;
+ my $layout_dir = $template->layout_dir;
+ if ( $page =~ m{^/\Q$layout_dir\E/} ) {
+ $app->response->has_passed(1);
+ return;
+ }
+
+ # remove leading '/', ensuring paths relative to the view
+ $page =~ s{^/}{};
+ my $view_path = $template->view_pathname($page);
+
+ if ( ! $template->pathname_exists( $view_path ) ) {
+ $app->response->has_passed(1);
+ return;
+ }
+
+ my $ct = $template->process( $page );
+ return ( $app->request->method eq 'GET' ) ? $ct : '';
+ };
+ }
+
+The C<code> method passed the L<Dancer2::Core::App> object which provides
+access to anything needed to process the request.
+
+A C<register> is then implemented to add the route to the registry and if
+the C<auto_page setting> is off, it does nothing.
+
+ sub register {
+ my ($self, $app) = @_;
+
+ return unless $app->config->{auto_page};
+
+ $app->add_route(
+ method => $_,
+ regexp => $self->regexp,
+ code => $self->code,
+ ) for $self->methods;
+ }
+
+The config parser looks for a C<route_handlers> section and any handler defined
+there is loaded. Thus, any random handler can be added to your app.
+For example, the default config file for any Dancer2 application is as follows:
+
+ route_handlers:
+ File:
+ public_dir: /path/to/public
+ AutoPage: 1
+
+=head1 ERRORS
+
+=head2 Error Pages
+
+When an HTTP error occurs (i.e. the action responds with a status code other
+than 200), this is how Dancer2 determines what page to display.
+
+=over
+
+=item * Looks in the C<views/> directory for a corresponding template file
+matching the error code (e.g. C<500.tt> or C<404.tt>). If such a file exists,
+it's used to report the error.
+
+=item * Next, looks in the C<public/> directory for a corresponding HTML file
+matching the error code (e.g. C<500.html> or C<404.html>). If such a file
+exists, it's used to report the error. (Note, however, that if B<show_errors>
+is set to true, in the case of a 500 error the static HTML page will not be
+shown, but will be replaced with a default error page containing more
+informative diagnostics. For more information see L<Dancer2::Config>.)
+
+=item * As default, render a generic error page on the fly.
+
+=back
+
+=head2 Execution Errors
+
+When an error occurs during the route execution, Dancer2 will render an
+error page with the HTTP status code 500.
+
+It's possible either to display the content of the error message or to hide
+it with a generic error page. This is a choice left to the end-user and can
+be controlled with the B<show_errors> setting (see above).
+
+Note that you can also choose to consider all warnings in your route
+handlers as errors when the setting B<warnings> is set to 1.
+
+=head2 Error Hooks
+
+When an error is caught by Dancer2's core, an exception object is built (of
+the class L<Dancer2::Core::Error>). This class provides a hook to let the
+user alter the error workflow if needed.
+
+C<init_error> hooks are called whenever an error object is built, the object
+is passed to the hook.
+
+ hook init_error => sub {
+ my $error = shift;
+ # do something with $error
+ };
+
+I<This hook was named B<before_error_init> in Dancer, both names currently
+are synonyms for backward-compatibility.>
+
+C<before_error> hooks are called whenever an error is going to be thrown, it
+receives the error object as its sole argument.
+
+ hook before_error => sub {
+ my $error = shift;
+ # do something with $error
+ };
+
+I<This hook was named B<before_error_render> in Dancer, both names currently
+are synonyms for backward-compatibility.>
+
+C<after_error> hooks are called whenever an error object has been thrown, it
+receives a L<Dancer2::Core::Response> object as its sole argument.
+
+ hook after_error => sub {
+ my $response = shift;
+ };
+
+I<This hook was named B<after_error_render> in Dancer, both names currently
+are synonyms for backward-compatibility.>
+
+C<on_route_exception> is called when an exception has been caught, at the
+route level, just before rethrowing it higher. This hook receives a
+L<Dancer2::Core::App> and the error as arguments.
+
+ hook on_route_exception => sub {
+ my ($app, $error) = @_;
+ };
+
+=head1 SESSIONS
+
+=head2 Handling sessions
+
+It's common to want to use sessions to give your web applications state; for
+instance, allowing a user to log in, creating a session, and checking that
+session on subsequent requests.
+
+By default Dancer 2 has L<Simple|Dancer2::Session::Simple> sessions enabled.
+It implements a very simple in-memory session storage. This will be fast and
+useful for testing, but such sessions will not persist between restarts of
+your app.
+
+If you'd like to use a different session engine you must declare it in the
+configuration file.
+
+For example to use YAML file base sessions you need to add the following
+to your F<config.yml>:
+
+ session: YAML
+
+Or, to enable session support from within your code,
+
+ set session => 'YAML';
+
+(However, controlling settings is best done from your config file.)
+
+The L<Dancer2::Session::YAML> backend implements a file-based YAML session
+storage to help with debugging, but shouldn't be used on production systems.
+
+There are other session backends, such as L<Dancer2::Session::Memcached>,
+which are recommended for production use.
+
+You can then use the L<session|Dancer2::Manual/session> keyword to manipulate the
+session:
+
+=head3 Storing data in the session
+
+Storing data in the session is as easy as:
+
+ session varname => 'value';
+
+=head3 Retrieving data from the session
+
+Retrieving data from the session is as easy as:
+
+ session('varname')
+
+Or, alternatively,
+
+ session->read("varname")
+
+=head3 Controlling where sessions are stored
+
+For disc-based session backends like L<Dancer2::Session::YAML>,
+session files are written to the session dir specified by
+the C<session_dir> setting, which defaults to C<./sessions>
+if not specifically set.
+
+If you need to control where session files are created, you can do so
+quickly and easily within your config file, for example:
+
+ session: YAML
+ engines:
+ session:
+ YAML:
+ session_dir: /tmp/dancer-sessions
+
+If the directory you specify does not exist, Dancer2 will attempt to create
+it for you.
+
+=head3 Changing session ID
+
+If you wish to change the session ID (for example on privilege level change):
+
+ my $new_session_id = app->change_session_id
+
+=head3 Destroying a session
+
+When you're done with your session, you can destroy it:
+
+ app->destroy_session
+
+=head2 Sessions and logging in
+
+A common requirement is to check the user is logged in, and, if not, require
+them to log in before continuing.
+
+This can easily be handled using a before hook to check their session:
+
+ use Dancer2;
+ set session => "Simple";
+
+ hook before => sub {
+ if (!session('user') && request->path !~ m{^/login}) {
+ forward '/login', { requested_path => request->path };
+ }
+ };
+
+ get '/' => sub { return "Home Page"; };
+
+ get '/secret' => sub { return "Top Secret Stuff here"; };
+
+ get '/login' => sub {
+ # Display a login page; the original URL they requested is available as
+ # query_parameters->get('requested_path'), so could be put in a hidden field in the form
+ template 'login', { path => query_parameters->get('requested_path') };
+ };
+
+ post '/login' => sub {
+ # Validate the username and password they supplied
+ if (body_parameters->get('user') eq 'bob' && body_parameters->get('pass') eq 'letmein') {
+ session user => body_parameters->get('user');
+ redirect body_parameters->get('path') || '/';
+ } else {
+ redirect '/login?failed=1';
+ }
+ };
+
+ dance();
+
+Here is what the corresponding C<login.tt> file should look like. You should
+place it in a directory called C<views/>:
+
+ <html>
+ <head>
+ <title>Session and logging in</title>
+ </head>
+ <body>
+ <form action='/login' method='POST'>
+ User Name : <input type='text' name='user'/>
+ Password: <input type='password' name='pass' />
+
+ <!-- Put the original path requested into a hidden
+ field so it's sent back in the POST and can be
+ used to redirect to the right page after login -->
+ <input type='hidden' name='path' value='<% path %>'/>
+
+ <input type='submit' value='Login' />
+ </form>
+ </body>
+ </html>
+
+Of course, you'll probably want to validate your users against a database
+table, or maybe via IMAP/LDAP/SSH/POP3/local system accounts via PAM etc.
+L<Authen::Simple> is probably a good starting point here!
+
+A simple working example of handling authentication against a database table
+yourself (using L<Dancer2::Plugin::Database> which provides the C<database>
+keyword, and L<Crypt::SaltedHash> to handle salted hashed passwords (well,
+you wouldn't store your users passwords in the clear, would you?)) follows:
+
+ post '/login' => sub {
+ my $user_value = body_parameters->get('user');
+ my $pass_value = body_parameters->get('pass');
+
+ my $user = database->quick_select('users',
+ { username => $user_value }
+ );
+ if (!$user) {
+ warning "Failed login for unrecognised user $user_value";
+ redirect '/login?failed=1';
+ } else {
+ if (Crypt::SaltedHash->validate($user->{password}, $pass_value))
+ {
+ debug "Password correct";
+ # Logged in successfully
+ session user => $user;
+ redirect body_parameters->get('path') || '/';
+ } else {
+ debug("Login failed - password incorrect for " . $user_value);
+ redirect '/login?failed=1';
+ }
+ }
+ };
+
+=head3 Retrieve complete hash stored in session
+
+Get complete hash stored in session:
+
+ my $hash = session;
+
+=head2 Writing a session engine
+
+In Dancer 2, a session backend consumes the role
+L<Dancer2::Core::Role::SessionFactory>.
+
+The following example using the Redis session demonstrates how session
+engines are written in Dancer 2.
+
+First thing to do is to create the class for the session engine,
+we'll name it C<Dancer2::Session::Redis>:
+
+ package Dancer2::Session::Redis;
+ use Moo;
+ with 'Dancer2::Core::Role::SessionFactory';
+
+we want our backend to have a handle over a Redis connection.
+To do that, we'll create an attribute C<redis>
+
+ use JSON;
+ use Redis;
+ use Dancer2::Core::Types; # brings helper for types
+
+ has redis => (
+ is => 'rw',
+ isa => InstanceOf['Redis'],
+ lazy => 1,
+ builder => '_build_redis',
+ );
+
+The lazy attribute says to Moo that this attribute will be
+built (initialized) only when called the first time. It means that
+the connection to Redis won't be opened until necessary.
+
+ sub _build_redis {
+ my ($self) = @_;
+ Redis->new(
+ server => $self->server,
+ password => $self->password,
+ encoding => undef,
+ );
+ }
+
+Two more attributes, C<server> and C<password> need to be created.
+We do this by defining them in the config file. Dancer2 passes anything
+defined in the config to the engine creation.
+
+ # config.yml
+ ...
+ engines:
+ session:
+ Redis:
+ server: foo.mydomain.com
+ password: S3Cr3t
+
+The server and password entries are now passed to the constructor
+of the Redis session engine and can be accessed from there.
+
+ has server => (is => 'ro', required => 1);
+ has password => (is => 'ro');
+
+Next, we define the subroutine C<_retrieve> which will return a session
+object for a session ID it has passed. Since in this case, sessions are
+going to be stored in Redis, the session ID will be the key, the session the value.
+So retrieving is as easy as doing a get and decoding the JSON string returned:
+
+ sub _retrieve {
+ my ($self, $session_id) = @_;
+ my $json = $self->redis->get($session_id);
+ my $hash = from_json( $json );
+ return bless $hash, 'Dancer2::Core::Session';
+ }
+
+The C<_flush> method is called by Dancer when the session needs to be stored in
+the backend. That is actually a write to Redis. The method receives a C<Dancer2::Core::Session>
+object and is supposed to store it.
+
+ sub _flush {
+ my ($self, $session) = @_;
+ my $json = encode_json( { %{ $session } } );
+ $self->redis->set($session->id, $json);
+ }
+
+For the C<_destroy> method which is supposed to remove a session from the backend,
+deleting the key from Redis is enough.
+
+ sub _destroy {
+ my ($self, $session_id) = @_;
+ $self->redis->del($session_id);
+ }
+
+The C<_sessions> method which is supposed to list all the session IDs currently
+stored in the backend is done by listing all the keys that Redis has.
+
+ sub _sessions {
+ my ($self) = @_;
+ my @keys = $self->redis->keys('*');
+ return \@keys;
+ }
+
+The session engine is now ready.
+
+=head3 The Session keyword
+
+Dancer2 maintains two session layers.
+
+The first layer, L<Dancer2::Core::Session> provides a session object
+which represents the current session. You can read from it as many
+times as you want, and write to it as many times as you want.
+
+The second layer is the session engine (L<Dancer2::Session::Simple>
+is one example), which is used in order to implement the reading and
+writing from the actual storage. This is read only once, when a request
+comes in (using a cookie whose value is C<dancer.session> by default).
+At the end of a request, all the data you've written will be flushed
+to the engine itself, which will do the actual write to the storage
+(whether it's in a hash in memory, in Memcache, or in a database).
+
+=head1 TEMPLATES
+
+Returning plain content is all well and good for examples or trivial apps,
+but soon you'll want to use templates to maintain separation between your
+code and your content. Dancer2 makes this easy.
+
+Your route handlers can use the L<template|Dancer2::Manual/template> keyword
+to render templates.
+
+=head2 Views
+
+In Dancer2, a file which holds a template is called a I<view>. Views are
+located in the C<appdir/views> directory.
+
+You can change this location by changing the setting 'views'. For instance
+if your templates are located in the 'templates' directory, do the
+following:
+
+ set views => path( app->location , 'templates' );
+
+By default, the internal template engine L<Dancer2::Template::Simple> is
+used, but you may want to upgrade to L<Template
+Toolkit|http://www.template-toolkit.org/>. If you do so, you have to enable
+this engine in your settings as explained in
+L<Dancer2::Template::TemplateToolkit> and you'll also have to install the
+L<Template> module.
+
+In order to render a view, just call the
+L<template|Dancer2::Manual/template> keyword at the end of the action by
+giving the view name and the HASHREF of tokens to interpolate in the view
+(note that for convenience, the request, session, params and vars are
+automatically accessible in the view, named C<request>, C<session>, C<params>,
+and C<vars>) - for example:
+
+ hook before => sub { var time => scalar(localtime) };
+
+ get '/hello/:name' => sub {
+ my $name = route_parameters->get('name');
+ template 'hello.tt', { name => $name };
+ };
+
+The template C<hello.tt> could contain, for example:
+
+ <p>Hi there, [% name %]!</p>
+ <p>You're using [% request.user_agent %]</p>
+ [% IF session.username %]
+ <p>You're logged in as [% session.username %]</p>
+ [% END %]
+ It's currently [% vars.time %]
+
+For a full list of the tokens automatically added to your template (like
+C<session>, C<request>, and C<vars>, refer to
+L<Dancer2::Core::Role::Template>).
+
+By default, views use a F<.tt> extension. This can be overridden by setting
+the C<extension> attribute in the template engine configuration:
+
+ set engines => {
+ template => {
+ template_toolkit => {
+ extension => 'foo',
+ },
+ },
+ };
+
+=head2 Layouts
+
+A layout is a special view, located in the F<layouts> directory (inside the
+views directory) which must have a token named C<content>. That token marks
+the place where to render the action view. This lets you define a global
+layout for your actions, and have each individual view contain only
+specific content. This is a good thing and helps avoid lots of needless
+duplication of HTML. :)
+
+For example, the layout F<views/layouts/main.tt>:
+
+ <html>
+ <head>...</head>
+ <body>
+ <div id="header">
+ ...
+ </div>
+
+ <div id="content">
+ [% content %]
+ </div>
+
+ </body>
+ </html>
+
+You can tell your app which layout to use with C<layout: name> in the config
+file, or within your code:
+
+ set layout => 'main';
+
+You can control which layout to use (or whether to use a layout at all) for
+a specific request without altering the layout setting by passing an options
+hashref as the third param to the template keyword:
+
+ template 'index.tt', {}, { layout => undef };
+
+If your application is not mounted under root (C</>), you can use a
+C<before_template_render> hook instead of hardcoding the path into your
+application for your CSS, images and JavaScript:
+
+ hook before_template_render => sub {
+ my $tokens = shift;
+ $tokens->{uri_base} = request->base->path;
+ };
+
+Then in your layout, modify your CSS inclusion as follows:
+
+ <link rel="stylesheet" href="[% uri_base %]/css/style.css" />
+
+From now on you can mount your application wherever you want, without any
+further modification of the CSS inclusion.
+
+=head2 Encoding
+
+If you use L<Plack> and have a Unicode problem with your Dancer2
+application, don't forget to check if you have set your template engine to
+use Unicode, and set the default charset to UTF-8. So, if you are using
+template toolkit, your config file will look like this:
+
+ charset: UTF-8
+ engines:
+ template:
+ template_toolkit:
+ ENCODING: utf8
+
+=head2 Default Template Variables
+
+Every template knows about the following variables, which are provided by
+L<Dancer2::Core::Role::Template>. Some are similar to the keywords you can
+use in the Perl part of your Dancer2 application.
+
+=over 4
+
+=item * B<perl_version>
+
+Current version of perl, effectively
+L<C<$^V>|http://perldoc.perl.org/perlvar.html#%24%5eV>.
+
+=item * B<dancer_version>
+
+Current version of Dancer2, effectively C<< Dancer2->VERSION >>.
+
+=item * B<settings>
+
+A hash of the application configuration. This is like
+the L<config|Dancer2::Manual/config> keyword.
+
+=item * B<request>
+
+The current request object. This is like the L<request|Dancer2::Manual/request> keyword.
+
+=item * B<params>
+
+A hash reference of all the parameters.
+
+Currently the equivalent of C<< $request->params >>, and like the
+L<params|Dancer2::Manual/params> keyword.
+
+=item * B<vars>
+
+The list of request variables, which is what you would get if you
+called the L<vars|Dancer2::Manual/vars> keyword.
+
+=item * B<session>
+
+The current session data, if a session exists. This is like
+the L<session|Dancer2::Manual/session> keyword.
+
+=back
+
+=head1 STATIC FILES
+
+=head2 Static Directory
+
+Static files are served from the F<./public> directory. You can specify a
+different location by setting the C<public_dir> option:
+
+ set public_dir => path( app->location , 'static' );
+
+When you modify default public_dir you have to set C<static_handler> option.
+
+ set static_handler => true;
+
+Note that the public directory name is not included in the URL. A file
+F<./public/css/style.css> is made available as
+L<http://example.com/css/style.css>.
+
+=head2 Static File from a Route Handler
+
+It's possible for a route handler to send a static file, as follows:
+
+ get '/download/*' => sub {
+ my ($file) = splat;
+
+ send_file $file;
+ };
+
+Or even if you want your index page to be a plain old F<index.html> file,
+just do:
+
+ get '/' => sub {
+ send_file '/index.html'
+ };
+
+=head1 FILE UPLOADS
+
+Files are uploaded in Dancer2 using the class L<Dancer2::Core::Request::Upload>.
+The objects are accessible within the route handlers using the C<upload>
+keyword:
+
+ post '/upload' => sub {
+ my $upload = upload('file_input_name'); # upload object
+ $upload->copy_to('Uploads/');
+ };
+
+=head1 CONFIGURATION
+
+=head2 Configuration and environments
+
+Configuring a Dancer2 application can be done in many ways. The easiest one
+(and maybe the dirtiest) is to put all your settings statements at the top
+of your script, before calling the C<dance()> method.
+
+Other ways are possible: for example, you can define all your settings in the file
+C<appdir/config.yml>. For this, you must have installed the L<YAML> module, and
+of course, write the config file in YAML.
+
+That's better than the first option, but it's still not perfect as you can't
+switch easily from an environment to another without rewriting the config
+file.
+
+A better solution is to have one F<config.yml> file with default global
+settings, like the following:
+
+ # appdir/config.yml
+ logger: 'file'
+ layout: 'main'
+
+And then write as many environment files as you like in
+C<appdir/environments>. That way, the appropriate environment config file
+will be loaded according to the running environment (if none is specified,
+it will be 'development').
+
+You can change the running environment when starting your app using the
+C<plackup> command's C<--env> or C<--E> switch:
+
+ plackup -E production bin/app.psgi
+
+Altenatively, you can set the
+L<C<DANCER_ENVIRONMENT>|https://metacpan.org/pod/Dancer2::Config#DANCER_ENVIRONMENT>
+environment variable in the shell or in your web server's configuration file.
+
+Typically, you'll want to set the following values in a development config
+file:
+
+ # appdir/environments/development.yml
+ log: 'debug'
+ startup_info: 1
+ show_errors: 1
+
+And in a production one:
+
+ # appdir/environments/production.yml
+ log: 'warning'
+ startup_info: 0
+ show_errors: 0
+
+Please note that you are not limited to writing configuration files in YAML.
+Dancer2 supports any file format that is supported by L<Config::Any>, such
+as JSON, XML, INI files, and Apache-style config files. See the Dancer2
+L<configuration guide|https://metacpan.org/pod/Dancer2::Config#MANIPULATING-SETTINGS-VIA-CONFIGURATION-FILES>
+for more information.
+
+=head2 Accessing configuration information
+
+A Dancer2 application can use the C<config> keyword to easily access the
+settings within its config file, for instance:
+
+ get '/appname' => sub {
+ return "This is " . config->{appname};
+ };
+
+This makes keeping your application's settings all in one place simple and
+easy - you shouldn't need to worry about implementing all that yourself. :)
+
+=head2 Settings
+
+It's possible to change almost every parameter of the application via the
+settings mechanism.
+
+A setting is a key/value pair assigned by the keyword B<set>:
+
+ set setting_name => 'setting_value';
+
+More usefully, settings can be defined in a configuration file.
+Environment-specific settings can also be defined in environment-specific
+files (for instance, you do not want to show error stacktraces in
+production, and might want extra logging in development).
+
+=head2 Serializers
+
+When writing a webservice, data serialization/deserialization is a common
+issue to deal with. Dancer2 can automatically handle that for you, via a
+serializer.
+
+When setting up a serializer, a new behaviour is authorized for any route
+handler you define: any non-scalar response will be rendered as a serialized
+string, via the current serializer.
+
+Here is an example of a route handler that will return a hashref:
+
+ use Dancer2;
+ set serializer => 'JSON';
+
+ get '/user/:id/' => sub {
+ { foo => 42,
+ number => 100234,
+ list => [qw(one two three)],
+ }
+ };
+
+Dancer2 will render the response via the current serializer.
+
+Hence, with the JSON serializer set, the route handler above would result in
+a content like the following:
+
+ {"number":100234,"foo":42,"list":["one","two","three"]}
+
+If you send a value which is validated serialized data, but is not in the
+form a key and value pair (such as a serialized string or a JSON array), the
+data will not be available in C<params> but will be available in
+C<< request->data >>.
+
+The following serializers are available, be aware they dynamically depend on
+Perl modules you may not have on your system.
+
+=over 4
+
+=item * B<JSON>
+
+Requires L<JSON>.
+
+=item * B<YAML>
+
+Requires L<YAML>,
+
+=item * B<XML>
+
+Requires L<XML::Simple>.
+
+=item * B<Mutable>
+
+Will try to find the appropriate serializer using the B<Content-Type> and
+B<Accept-type> header of the request.
+
+=back
+
+=head2 Importing using Appname
+
+An app in Dancer2 uses the class name (defined by the C<package> function) to
+define the App name. Thus separating the App to multiple files, actually means
+creating multiple applications. This means that any engine defined in an application,
+because the application is a complete separate scope, will not be available to a
+different application:
+
+ package MyApp::User {
+ use Dancer2;
+ set serializer => 'JSON';
+ get '/view' => sub {...};
+ }
+
+ package MyApp::User::Edit {
+ use Dancer2;
+ get '/edit' => sub {...};
+ }
+
+These are two different Dancer2 Apps. They have different scopes, contexts,
+and thus different engines. While C<MyApp::User> has a serializer defined,
+C<MyApp::User::Edit> will not have that configuration.
+
+By using the import option C<appname>, we can ask Dancer2 to extend an
+App without creating a new one:
+
+ package MyApp::User {
+ use Dancer2;
+ set serializer => 'JSON';
+ get '/view' => sub {...};
+ }
+
+ package MyApp::User::Edit {
+ use Dancer2 appname => 'MyApp::User'; # extending MyApp::User
+ get '/edit' => sub {...};
+ }
+
+The import option C<appname> allows you to seamlessly extend Dancer2 Apps
+without creating unnecessary additional applications or repeat any definitions.
+This allows you to spread your application routes across multiple files and allow
+ease of mind when developing it, and accommodate multiple developers working
+on the same codebase.
+
+ # app.pl
+ use MyApp::User;
+ use MyApp::User::Edit;
+
+ # single application composed of routes provided in multiple files
+ MyApp::User->to_app;
+
+This way only one class needs to be loaded while creating an app:
+
+ # app.pl:
+ use MyApp::User;
+ MyApp::User->to_app;
+
+=head1 LOGGING
+
+=head2 Configuring logging
+
+It's possible to log messages generated by the application and by Dancer2
+itself.
+
+To start logging, select the logging engine you wish to use with the
+C<logger> setting; Dancer2 includes built-in log engines named C<file> and
+C<console>, which log to a logfile and to the console respectively.
+
+To enable logging to a file, add the following to your config file:
+
+ logger: 'file'
+
+Then you can choose which kind of messages you want to actually log:
+
+ log: 'core' # will log debug, info, warnings, errors,
+ # and messages from Dancer2 itself
+ log: 'debug' # will log debug, info, warning and errors
+ log: 'info' # will log info, warning and errors
+ log: 'warning' # will log warning and errors
+ log: 'error' # will log only errors
+
+If you're using the C<file> logging engine, a directory C<appdir/logs> will
+be created and will host one logfile per environment. The log message
+contains the time it was written, the PID of the current process, the
+message and the caller information (file and line).
+
+=head2 Logging your own messages
+
+Just call L<debug|https://metacpan.org/pod/Dancer2::Manual#debug>,
+L<info|https://metacpan.org/pod/Dancer2::Manual#info>,
+L<warning|https://metacpan.org/pod/Dancer2::Manual#warning> or
+L<error|https://metacpan.org/pod/Dancer2::Manual#error> with your message:
+
+ debug "This is a debug message from my app.";
+
+=head1 TESTING
+
+=head2 Using Plack::Test
+
+L<Plack::Test> receives a common web request (using standard L<HTTP::Request>
+objects), fakes a web server in order to create a proper PSGI request, and sends it
+to the web application. When the web application returns a PSGI response
+(which Dancer applications do), it will then convert it to a common web response
+(as a standard L<HTTP::Response> object).
+
+This allows you to then create requests in your test, create the code reference
+for your web application, call them, and receive a response object, which can
+then be tested.
+
+=head3 Basic Example
+
+Assuming there is a web application:
+
+ # MyApp.pm
+ package MyApp;
+ use Dancer2;
+ get '/' => sub {'OK'};
+ 1;
+
+The following test I<base.t> is created:
+
+ # base.t
+ use strict;
+ use warnings;
+ use Test::More tests => 2;
+ use Plack::Test;
+ use HTTP::Request;
+ use MyApp;
+
+Creating a coderef for the application using the C<to_app> keyword:
+
+ my $app = MyApp->to_app;
+
+Creating a test object from L<Plack::Test> for the application:
+
+ my $test = Plack::Test->create($app);
+
+Creating the first request object and sending it to the test object
+to receive a response:
+
+ my $request = HTTP::Request->new( GET => '/' );
+ my $response = $test->request($request);
+
+It can now be tested:
+
+ ok( $response->is_success, '[GET /] Successful request' );
+ is( $response->content, 'OK', '[GET /] Correct content' );
+
+=head3 Putting it together
+
+ # base.t
+ use strict;
+ use warnings;
+ use Test::More;
+ use Plack::Test;
+ use HTTP::Request::Common;
+ use MyApp;
+
+ my $test = Plack::Test->create( MyApp->to_app );
+ my $response = $test->request( GET '/' );
+
+ ok( $response->is_success, '[GET /] Successful request' );
+ is( $response->content, 'OK', '[GET /] Correct content' );
+
+ done_testing();
+
+=head3 Subtests
+
+Tests can be separated using L<Test::More>'s C<subtest> functionality,
+thus creating multiple self-contained tests that don't overwrite each other.
+
+Assuming we have a different app that has two states we want to test:
+
+ # MyApp.pm
+ package MyApp;
+ use Dancer2;
+ set serializer => 'JSON';
+
+ get '/:user' => sub {
+ my $user = route_parameters->get('user');
+
+ $user and return { user => $user };
+
+ return {};
+ };
+
+ 1;
+
+This is a contrived example of a route that checks for a user
+parameter. If it exists, it returns it in a hash with the key
+'user'. If not, it returns an empty hash
+
+ # param.t
+ use strict;
+ use warnings;
+ use Test::More;
+ use Plack::Test;
+ use HTTP::Request::Common;
+ use MyApp;
+
+ my $test = Plack::Test->create( MyApp->to_app );
+
+ subtest 'A empty request' => sub {
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content '{}', 'Empty response back' );
+ };
+
+ subtest 'Request with user' => sub {
+ my $res = $test->request( GET '/?user=sawyer_x' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content '{"user":"sawyer_x"}', 'Empty response back' );
+ };
+
+ done_testing();
+
+=head3 Cookies
+
+To handle cookies, which are mostly used for maintaining sessions,
+the following modules can be used:
+
+=over 4
+
+=item * L<Test::WWW::Mechanize::PSGI>
+
+=item * L<LWP::Protocol::PSGI>
+
+=item * L<HTTP::Cookies>
+
+=back
+
+Taking the previous test, assuming it actually creates and uses
+cookies for sessions:
+
+ # ... all the use statements
+ use HTTP::Cookies;
+
+ my $jar = HTTP::Cookies->new;
+ my $test = Plack::Test->create( MyApp->to_app );
+
+ subtest 'A empty request' => sub {
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content '{}', 'Empty response back' );
+ $jar->extract_cookies($res);
+ ok( $jar->as_string, 'We have cookies!' );
+ };
+
+ subtest 'Request with user' => sub {
+ my $req = GET '/?user=sawyer_x';
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content '{"user":"sawyer_x"}', 'Empty response back' );
+ $jar->extract_cookies($res);
+
+ ok( ! $jar->as_string, 'All cookies deleted' );
+ };
+
+ done_testing();
+
+Here a cookie jar is created, all requests and responses, existing
+cookies, as well as cookies that were deleted by the response, are checked.
+
+=head3 Accessing the configuration file
+
+By importing Dancer2 in the command line scripts, there is full
+access to the configuration using the imported keywords:
+
+ use strict;
+ use warnings;
+ use Test::More;
+ use Plack::Test;
+ use HTTP::Request::Common;
+ use MyApp;
+ use Dancer2;
+
+ my $appname = config->{'appname'};
+ diag "Testing $appname";
+
+ # ...
+
+=head1 PACKAGING
+
+=head2 Carton
+
+=head3 What it does
+
+L<Carton> sets up a local copy of your project prerequisites. You only
+need to define them in a file and ask Carton to download all of them
+and set them up.
+When you want to deploy your app, you just carry the git clone and ask
+Carton to set up the environment again and you will then be able to run it.
+
+The benefits are multifold:
+
+=over 4
+
+=item * Local Directory copy
+
+By putting all the dependencies in a local directory, you can make
+sure they aren't updated by someone else by accident and their versions
+locked to the version you picked.
+
+=item * Sync versions
+
+Deciding which versions of the dependent modules your project needs
+allows you to sync this with other developers as well. Now you're all
+using the same version and they don't change unless you want update the
+versions you want. When updated everyone again uses the same new version
+of everything.
+
+=item * Carry only the requirement, not bundled modules
+
+Instead of bundling the modules, you only actually bundle the requirements.
+Carton builds them for you when you need it.
+
+=back
+
+=head3 Setting it up
+
+First set up a new app:
+
+ $ dancer2 -a MyApp
+ ...
+
+Delete the files that are not needed:
+
+ $ rm -f Makefile.PL MANIFEST MANIFEST.SKIP
+
+Create a git repo:
+
+ $ git init && git add . && git commit -m "initial commit"
+
+Add a requirement using the L<cpanfile> format:
+
+ $ cat > cpanfile
+ requires 'Dancer2' => 0.155000;
+ requires 'Template' => 0;
+ recommends 'URL::Encode::XS' => 0;
+ recommends 'CGI::Deurl::XS' => 0;
+ recommends 'HTTP::Parser::XS' => 0;
+
+Ask carton to set it up:
+
+ $ carton install
+ Installing modules using [...]
+ Successfully installed [...]
+ ...
+ Complete! Modules were install into [...]/local
+
+Now we have two files: I<cpanfile> and I<cpanfile.snapshot>. We
+add both of them to our Git repository and we make sure we don't
+accidentally add the I<local/> directory Carton created which holds
+the modules it installed:
+
+ $ echo local/ >> .gitignore
+ $ git add .gitignore cpanfile cpanfile.snapshot
+ $ git commit -m "Start using carton"
+
+When we want to update the versions on the production machine,
+we simply call:
+
+ $ carton install --deployment
+
+By using --deployment we make sure we only install the modules
+we have in our cpanfile.snapshot file and do not fallback to querying
+the CPAN.
+
+=head2 FatPacker
+
+L<App::FatPacker> (using its command line interface, L<fatpack>) packs
+dependencies into a single file, allowing you to carry a single file
+instead of a directory tree.
+
+As long as your application is pure-Perl, you could create a single
+file with your application and all of Dancer2 in it.
+
+The following example will demonstrate how this can be done:
+
+Assuming we have an application in I<lib/MyApp.pm>:
+
+ package MyApp;
+ use Dancer2;
+ get '/' => sub {'OK'};
+ 1;
+
+And we have a handler in I<bin/app.pl>:
+
+ use strict;
+ use warnings;
+ use FindBin;
+ use lib "$FindBin::Bin/../lib";
+ use MyApp;
+
+ MyApp->to_app;
+
+To fatpack it, we begin by tracing the script:
+
+ $ fatpack trace bin/app.pl
+
+This creates a I<fatpacker.trace> file. From this we create the packlists:
+
+ $ fatpack packlists-for `cat fatpacker.trace` > packlists
+
+The packlists are stored in a file called I<packlists>.
+
+Now we create the tree using the following command:
+
+ $ fatpack tree `cat packlists`
+
+The tree is created under the directory I<fatlib>.
+
+Now we create a file containing the dependency tree, and add our script to it,
+using the following command:
+
+ $ (fatpack file; cat bin/app.pl) > myapp.pl
+
+This creates a file called I<myapp.pl> with everything in it. Dancer2 uses
+L<MIME::Types> which has a database of all MIME types and helps translate those.
+The small database file containing all of these types is a binary and therefore
+cannot be fatpacked. Hence, it needs to be copied to the current directory so our
+script can find it:
+
+ $ cp fatlib/MIME/types.db .
+
+=head1 MIDDLEWARES
+
+=head2 Plack middlewares
+
+If you want to use Plack middlewares, you need to enable them using
+L<Plack::Builder> as such:
+
+ # in app.psgi or any other handler
+ use MyApp;
+ use Plack::Builder;
+
+ builder {
+ enable 'Deflater';
+ enable 'Session', store => 'File';
+ enable 'Debug', panels => [ qw<DBITrace Memory Timer> ];
+ MyApp->to_app;
+ };
+
+The nice thing about this setup is that it will work seamlessly through
+Plack or through the internal web server.
+
+ # load dev web server (without middlewares)
+ perl -Ilib app.psgi
+
+ # load plack web server (with middlewares)
+ plackup -I lib app.psgi
+
+You do not need to provide different files for either server.
+
+=head3 Path-based middlewares
+
+If you want to set up a middleware for a specific path, you can do that using
+L<Plack::Builder> which uses L<Plack::App::URLMap>:
+
+ # in your app.psgi or any other handler
+ use MyApp;
+ use Plack::Builder;
+
+ my $special_handler = sub { ... };
+
+ builder {
+ mount '/special' => $special_handler;
+ mount '/' => MyApp->to_app;
+ };
+
+=head3 Removing default middlewares
+
+By default, a Dancer2 app is automatically wrapped with the following middleware
+
+=over 4
+
+=item * L<Plack::Middleware::FixMissingBodyInRedirect>
+
+=item * L<Plack::Middleware::Head>
+
+=back
+
+You can configure the setting C<no_default_middleware> to a true value to stop your
+Dancer2 app being wrapped with these default middleware layers.
+
+ # in you Dancer2 app or config.yml
+ package MyApp;
+ use Dancer2
+
+ set no_default_middleware => true;
+
+This is necessary if you need to add eTag or ContentMD5 headers to
+C<HEAD> requests, and you are encouraged to manually add those default
+middleware back into your PSGI stack.
+
+=head3 Running on Perl web servers with plackup
+
+A number of Perl web servers supporting PSGI are available on CPAN:
+
+=over 4
+
+=item * L<Starman>
+
+C<Starman> is a high performance web server, with support for preforking,
+signals, multiple interfaces, graceful restarts and dynamic worker pool
+configuration.
+
+=item * L<Twiggy>
+
+C<Twiggy> is an C<AnyEvent> web server, it's light and fast.
+
+=item * L<Corona>
+
+C<Corona> is a C<Coro> based web server.
+
+=back
+
+To start your application, just run plackup (see L<Plack> and specific
+servers above for all available options):
+
+ $ plackup bin/app.psgi
+ $ plackup -E deployment -s Starman --workers=10 -p 5001 -a bin/app.psgi
+
+As you can see, the scaffolded Perl script for your app can be used as a
+PSGI startup file.
+
+=head4 Enabling content compression
+
+Content compression (gzip, deflate) can be easily enabled via a Plack
+middleware (see L<Plack/Plack::Middleware>): L<Plack::Middleware::Deflater>.
+It's a middleware to encode the response body in gzip or deflate, based on the
+C<Accept-Encoding> HTTP request header.
+
+Enable it as you would enable any Plack middleware. First you need to
+install L<Plack::Middleware::Deflater>, then in the handler (usually
+F<app.psgi>) edit it to use L<Plack::Builder>, as described above:
+
+ use Dancer2;
+ use MyApp;
+ use Plack::Builder;
+
+ builder {
+ enable 'Deflater';
+ MyApp->to_app;
+ };
+
+To test if content compression works, trace the HTTP request and response
+before and after enabling this middleware. Among other things, you should
+notice that the response is gzip or deflate encoded, and contains a header
+C<Content-Encoding> set to C<gzip> or C<deflate>.
+
+=head3 Running multiple apps with Plack::Builder
+
+You can use L<Plack::Builder> to mount multiple Dancer2 applications on a
+L<PSGI> webserver like L<Starman>.
+
+Start by creating a simple app.psgi file:
+
+ use OurWiki; # first app
+ use OurForum; # second app
+ use Plack::Builder;
+
+ builder {
+ mount '/wiki' => OurWiki->to_app;
+ mount '/forum' => OurForum->to_app;
+ };
+
+and now use L<Starman>
+
+ plackup -a app.psgi -s Starman
+
+Currently this still demands the same appdir for both (default circumstance)
+but in a future version this will be easier to change while staying very
+simple to mount.
+
+=head3 Running from Apache with Plack
+
+You can run your app from Apache using PSGI (Plack), with a config like the
+following:
+
+ <VirtualHost myapp.example.com>
+ ServerName www.myapp.example.com
+ ServerAlias myapp.example.com
+ DocumentRoot /websites/myapp.example.com
+
+ <Directory /home/myapp/myapp>
+ AllowOverride None
+ Order allow,deny
+ Allow from all
+ </Directory>
+
+ <Location />
+ SetHandler perl-script
+ PerlResponseHandler Plack::Handler::Apache2
+ PerlSetVar psgi_app /websites/myapp.example.com/app.psgi
+ </Location>
+
+ ErrorLog /websites/myapp.example.com/logs/error_log
+ CustomLog /websites/myapp.example.com/logs/access_log common
+ </VirtualHost>
+
+To set the environment you want to use for your application (production or
+development), you can set it this way:
+
+ <VirtualHost>
+ ...
+ SetEnv DANCER_ENVIRONMENT "production"
+ ...
+ </VirtualHost>
+
+=head1 PLUGINS
+
+=head2 Writing a plugin
+
+See L<Dancer2::Plugin/Writing the plugin> for information on how to author
+a new plugin for Dancer2.
+
+=head1 EXPORTS
+
+By default, C<use Dancer2> exports all the DSL keywords and sets up the
+webapp under the name of the current package. The following tags control
+exports and webapp namespace.
+
+=over 4
+
+=item * B<!keyword>
+
+If you want to prevent Dancer2 from exporting specific keywords (perhaps you
+plan to implement them yourself in a different way, or they clash with
+another module you're loading), you can simply exclude them:
+
+ use Test::More;
+ use Dancer2 qw(!pass);
+
+The above would import all keywords as usual, with the exception of C<pass>.
+
+=item * B<appname>
+
+A larger application may split its source between several packages to aid
+maintainability. Dancer2 will create a separate application for each
+package, each having separate hooks, config and/or engines. You can force
+Dancer2 to collect the route and hooks into a single application with the
+C<appname> tag; e.g.
+
+ package MyApp;
+ use Dancer2;
+ get '/foo' => sub {...};
+
+ package MyApp::Private;
+ use Dancer2 appname => MyApp;
+ get '/bar' => sub {...};
+
+The above would add the C<bar> route to the MyApp application. Dancer2 will
+I<not> create an application with the name C<MyApp::Private>.
+
+=item * B<:nopragmas>
+
+By default L<Dancer2> will import three pragmas: L<strict>, L<warnings>,
+and L<utf8>. If you require control over the imported pragmas, you can add
+B<:nopragmas> to the importing flags, in which case Dancer2 will not import
+any pragmas:
+
+ use strict;
+ use warnings;
+ no warnings 'experimental::smartmatch'; # for example...
+ use Dancer2 ':nopragmas'; # do not touch the existing pragmas
+
+This way importing C<Dancer2> does not change the existing pragmas setup
+you have.
+
+=back
+
+When you C<use Dancer2>, you get an C<import> method added into the current
+package. This B<will> override previously declared import methods from other
+sources, such as L<Exporter>. Dancer2 applications support the following
+tags on import:
+
+=over 4
+
+=item * B<with>
+
+The C<with> tag allows an app to pass one or more config entries to another
+app, when it C<use>s it.
+
+ package MyApp;
+ use Dancer2;
+
+ BEGIN { set session => 'YAML' };
+ use Blog with => { session => engine('session') };
+
+In this example, the session engine is passed to the C<Blog> app. That way,
+anything done in the session will be shared between both apps.
+
+Anything that is defined in the config entry can be passed that way. If we
+want to pass the whole config object, it can be done like so:
+
+ use SomeApp with => { %{config()} };
+
+=back
+
+=head1 DSL KEYWORDS
+
+Dancer2 provides you with a DSL (Domain-Specific Language) which makes
+implementing your web application trivial.
+
+For example, take the following example:
+
+ use Dancer2;
+
+ get '/hello/:name' => sub {
+ my $name = route_parameters->get('name');
+ };
+ dance;
+
+C<get> and C<route_parameters> are keywords provided by Dancer2.
+
+This document lists all keywords provided by Dancer2. It does not cover
+additional keywords which may be provided by loaded plugins; see the
+documentation for plugins you use to see which additional keywords they make
+available to you.
+
+=head2 any
+
+Defines a route for multiple HTTP methods at once:
+
+ any ['get', 'post'] => '/myaction' => sub {
+ # code
+ };
+
+Or even, a route handler that would match any HTTP methods:
+
+ any '/myaction' => sub {
+ # code
+ };
+
+=head2 cookies
+
+Accesses cookies values, it returns a hashref of L<Dancer2::Core::Cookie>
+objects:
+
+ get '/some_action' => sub {
+ my $cookie = cookies->{name};
+ return $cookie->value;
+ };
+
+In case you have stored something other than a scalar in your cookie:
+
+ get '/some_action' => sub {
+ my $cookie = cookies->{oauth};
+ my %values = $cookie->value;
+ return ($values{token}, $values{token_secret});
+ };
+
+=head2 cookie
+
+Accesses a cookie value (or sets it). Note that this method will eventually
+be preferred over C<set_cookie>.
+
+ cookie lang => "fr-FR"; # set a cookie and return its value
+ cookie lang => "fr-FR", expires => "2 hours"; # extra cookie info
+ cookie "lang" # return a cookie value
+
+If your cookie value is a key/value URI string, like
+
+ token=ABC&user=foo
+
+C<cookie> will only return the first part (C<token=ABC>) if called in scalar
+context. Use list context to fetch them all:
+
+ my @values = cookie "name";
+
+=head2 config
+
+Accesses the configuration of the application:
+
+ get '/appname' => sub {
+ return "This is " . config->{appname};
+ };
+
+=head2 content
+
+Sets the content for the response. This B<only> works within a delayed
+response.
+
+This will crash:
+
+ get '/' => sub {
+ # THIS WILL CRASH
+ content 'Hello, world!';
+ };
+
+But this will work just fine:
+
+ get '/' => sub {
+ delayed {
+ content 'Hello, world!';
+ ...
+ };
+ };
+
+=head2 content_type
+
+Sets the B<content-type> rendered, for the current route handler:
+
+ get '/cat/:txtfile' => sub {
+ content_type 'text/plain';
+
+ # here we can dump the contents of route_parameters->get('txtfile')
+ };
+
+You can use abbreviations for content types. For instance:
+
+ get '/svg/:id' => sub {
+ content_type 'svg';
+
+ # here we can dump the image with id route_parameters->get('id')
+ };
+
+Note that if you want to change the default content-type for every route,
+it is easier to change the C<content_type> setting instead.
+
+=head2 dance
+
+Alias for the C<start> keyword.
+
+=head2 dancer_version
+
+Returns the version of Dancer. If you need the major version, do something
+like:
+
+ int(dancer_version);
+
+=head2 debug
+
+Logs a message of debug level:
+
+ debug "This is a debug message";
+
+See L<Dancer2::Core::Role::Logger> for details on how to configure where log
+messages go.
+
+=head2 decode_json ($string)
+
+Deserializes a JSON structure from an UTF-8 binary string.
+
+=head2 dirname
+
+Returns the dirname of the path given:
+
+ my $dir = dirname($some_path);
+
+=head2 encode_json ($structure)
+
+Serializes a structure to a UTF-8 binary JSON string.
+
+Calling this function will B<not> trigger the serialization's hooks.
+
+=head2 engine
+
+Given a namespace, returns the current engine object
+
+ my $template_engine = engine 'template';
+ my $html = $template_engine->apply_renderer(...);
+ $template_engine->apply_layout($html);
+
+=head2 error
+
+Logs a message of error level:
+
+ error "This is an error message";
+
+See L<Dancer2::Core::Role::Logger> for details on how to configure where log
+messages go.
+
+=head2 false
+
+Constant that returns a false value (0).
+
+=head2 forward
+
+Runs an "internal redirect" of the current route to another route. More
+formally; when C<forward> is executed, the current dispatch of the route is
+aborted, the request is modified (altering query params or request method),
+and the modified request following a new route is dispatched again. Any
+remaining code (route and hooks) from the current dispatch will never be run
+and the modified route's dispatch will execute hooks for the new route normally.
+
+It effectively lets you chain routes together in a clean manner.
+
+ get '/demo/articles/:article_id' => sub {
+
+ # you'll have to implement this next sub yourself :)
+ change_the_main_database_to_demo();
+
+ forward "/articles/" . route_parameters->get('article_id');
+ };
+
+In the above example, the users that reach I</demo/articles/30> will
+actually reach I</articles/30> but we've changed the database to demo
+before.
+
+This is pretty cool because it lets us retain our paths and offer a demo
+database by merely going to I</demo/...>.
+
+You'll notice that in the example we didn't indicate whether it was B<GET>
+or B<POST>. That is because C<forward> chains the same type of route the
+user reached. If it was a B<GET>, it will remain a B<GET> (but if you do
+need to change the method, you can do so; read on below for details.)
+
+Also notice that C<forward> only redirects to a new route. It does not redirect
+the requests involving static files. This is because static files are handled
+before L<Dancer2> tries to match the request to a route - static files take
+higher precedence.
+
+This means that you will not be able to C<forward> to a static file. If you
+wish to do so, you have two options: either redirect (asking the browser to
+make another request, but to a file path instead) or use C<send_file> to
+provide a file.
+
+B<WARNING:> Any code after a C<forward> is ignored, until the end of the
+route. It's not necessary to use C<return> with C<forward> anymore.
+
+ get '/foo/:article_id' => sub {
+ if ($condition) {
+ forward "/articles/" . route_parameters->get('article_id');
+ # The following code WILL NOT BE executed
+ do_stuff();
+ }
+
+ more_stuff();
+ };
+
+Note that C<forward> doesn't parse GET arguments. So, you can't use
+something like:
+
+ forward '/home?authorized=1';
+
+But C<forward> supports an optional hashref with parameters to be added to
+the actual parameters:
+
+ forward '/home', { authorized => 1 };
+
+Finally, you can add some more options to the C<forward> method, in a third
+argument, also as a hashref. That option is currently only used to change
+the method of your request. Use with caution.
+
+ forward '/home', { auth => 1 }, { method => 'POST' };
+
+=head2 from_dumper ($structure)
+
+Deserializes a Data::Dumper structure.
+
+=head2 from_json ($string, \%options)
+
+Deserializes a JSON structure from a string. You should probably use
+C<decode_json> which expects a UTF-8 encoded binary string and
+handles decoding it for you.
+
+=head2 from_yaml ($structure)
+
+Deserializes a YAML structure.
+
+=head2 get
+
+Defines a route for HTTP B<GET> requests to the given path:
+
+ get '/' => sub {
+ return "Hello world";
+ }
+
+Note that a route to match B<HEAD> requests is automatically created as well.
+
+=head2 halt
+
+Sets a response object with the content given.
+
+When used as a return value from a hook, this breaks the execution flow and
+renders the response immediately:
+
+ hook before => sub {
+ if ($some_condition) {
+ halt("Unauthorized");
+
+ # this code is not executed
+ do_stuff();
+ }
+ };
+
+ get '/' => sub {
+ "hello there";
+ };
+
+B<WARNING:> Issuing a halt immediately exits the current route, and performs
+the halt. Thus, any code after a halt is ignored, until the end of the route.
+Hence, it's not necessary anymore to use C<return> with halt.
+
+=head2 response_headers
+
+Adds custom headers to response:
+
+ get '/send/headers', sub {
+ response_headers 'X-Foo' => 'bar', 'X-Bar' => 'foo';
+ }
+
+=head2 response_header
+
+Adds a custom header to response:
+
+ get '/send/header', sub {
+ response_header 'x-my-header' => 'shazam!';
+ }
+
+Note that it will overwrite the old value of the header, if any. To avoid
+that, see L</push_response_header>.
+
+=head2 push_response_header
+
+Do the same as C<response_header>, but allow for multiple headers with the same
+name.
+
+ get '/send/header', sub {
+ push_response_header 'x-my-header' => '1';
+ push_response_header 'x-my-header' => '2';
+ # will result in two headers "x-my-header" in the response
+ }
+
+=head2 prepare_app
+
+You can introduce code you want to run when your app is loaded, similar to the
+C<prepare_app> in L<Plack::Middleware>.
+
+ prepare_app {
+ my $app = shift;
+
+ ... # do your thing
+ };
+
+You should not close over the App instance, since you receive it as a first
+argument. If you close over it, you B<will> have a memory leak.
+
+ my $app = app();
+
+ prepare_app {
+ do_something_with_app($app); # MEMORY LEAK
+ };
+
+=head2 hook
+
+Adds a hook at some position. For example :
+
+ hook before_serializer => sub {
+ my $content = shift;
+ ...
+ };
+
+There can be multiple hooks assigned to a given position, and each will be
+executed in order.
+
+See the L<HOOKS|/HOOKS> section for a list of available hooks.
+
+=head2 info
+
+Logs a message of C<info> level:
+
+ info "This is an info message";
+
+See L<Dancer2::Core::Role::Logger> for details on how to configure where log
+messages go.
+
+=head2 mime
+
+Shortcut to access the instance object of L<Dancer2::Core::MIME>. You should
+read the L<Dancer2::Core::MIME> documentation for full details, but the most
+commonly-used methods are summarized below:
+
+ # set a new mime type
+ mime->add_type( foo => 'text/foo' );
+
+ # set a mime type alias
+ mime->add_alias( f => 'foo' );
+
+ # get mime type for an alias
+ my $m = mime->for_name( 'f' );
+
+ # get mime type for a file (based on extension)
+ my $m = mime->for_file( "foo.bar" );
+
+ # get current defined default mime type
+ my $d = mime->default;
+
+ # set the default mime type using config.yml
+ # or using the set keyword
+ set default_mime_type => 'text/plain';
+
+=head2 params
+
+I<This method should be called from a route handler>.
+It's an alias for the L<Dancer2::Core::Request params
+accessor|Dancer2::Core::Request/"params($source)">. It returns a hash (in
+list context) or a hash reference (in scalar context) to all defined
+parameters. Check C<param> below to access quickly to a single parameter
+value.
+
+ post '/login' => sub {
+ # get all parameters as a single hash
+ my %all_parameters = params;
+
+ // request all parmameters from a specific source: body, query, route
+ my %body_parameters = params('body');
+ my %route_parameters = params('route');
+ my %query_parameters = params('query');
+
+ # any $source that is not body, query, or route generates an exception
+ params('fake_source'); // Unknown source params "fake_source"
+ };
+
+We now recommend using one of the specific keywords for parameters
+(C<route_parameters>, C<query_parameters>, and C<body_parameters>)
+instead of C<params> or C<param>.
+
+=head2 param
+
+I<This method should be called from a route handler>.
+This method is an accessor to the parameters hash table.
+
+ post '/login' => sub {
+ my $username = param "user";
+ my $password = param "pass";
+ # ...
+ };
+
+We now recommend using one of the specific keywords for parameters
+(C<route_parameters>, C<query_parameters>, and C<body_parameters>)
+instead of C<params> or C<param>.
+
+=head2 route_parameters
+
+Returns a L<Hash::MultiValue> object from the route parameters.
+
+ # /hello
+ get '/:foo' => sub {
+ my $foo = route_parameters->get('foo');
+ };
+
+=head2 query_parameters
+
+Returns a L<Hash::MultiValue> object from the request parameters.
+
+ /?foo=hello
+ get '/' => sub {
+ my $name = query_parameters->get('foo');
+ };
+
+ /?name=Alice&name=Bob
+ get '/' => sub {
+ my @names = query_parameters->get_all('name');
+ };
+
+=head2 body_parameters
+
+Returns a L<Hash::MultiValue> object from the body parameters.
+
+ post '/' => sub {
+ my $last_name = body_parameters->get('name');
+ my @all_names = body_parameters->get_all('name');
+ };
+
+=head2 pass
+
+I<This method should be called from a route handler>.
+Tells Dancer2 to pass the processing of the request to the next matching
+route.
+
+B<WARNING:> Issuing a pass immediately exits the current route, and performs
+the pass. Thus, any code after a pass is ignored, until the end of the
+route. Hence, it's not necessary anymore to use C<return> with pass.
+
+ get '/some/route' => sub {
+ if (...) {
+ # we want to let the next matching route handler process this one
+ pass(...);
+
+ # this code will be ignored
+ do_stuff();
+ }
+ };
+
+B<WARNING:> You cannot set the content before passing and have it remain,
+even if you use the C<content> keyword or set it directly in the response
+object.
+
+=head2 patch
+
+Defines a route for HTTP B<PATCH> requests to the given URL:
+
+ patch '/resource' => sub { ... };
+
+(C<PATCH> is a relatively new and not-yet-common HTTP verb, which is
+intended to work as a "partial-PUT", transferring just the changes; please
+see L<RFC5789|http://tools.ietf.org/html/rfc5789> for further details.)
+
+=head2 path
+
+Concatenates multiple paths together, without worrying about the underlying
+operating system:
+
+ my $path = path(dirname($0), 'lib', 'File.pm');
+
+It also normalizes (cleans) the path aesthetically. It does not verify whether
+the path exists, though.
+
+=head2 post
+
+Defines a route for HTTP B<POST> requests to the given URL:
+
+ post '/' => sub {
+ return "Hello world";
+ }
+
+=head2 prefix
+
+Defines a prefix for each route handler, like this:
+
+ prefix '/home';
+
+From here, any route handler is defined to /home/*:
+
+ get '/page1' => sub {}; # will match '/home/page1'
+
+You can unset the prefix value:
+
+ prefix undef;
+ get '/page1' => sub {}; # will match /page1
+
+For a safer alternative you can use lexical prefix like this:
+
+ prefix '/home' => sub {
+ ## Prefix is set to '/home' here
+
+ get ...;
+ get ...;
+ };
+ ## prefix reset to the previous version here
+
+This makes it possible to nest prefixes:
+
+ prefix '/home' => sub {
+ ## some routes
+
+ prefix '/private' => sub {
+ ## here we are under /home/private...
+
+ ## some more routes
+ };
+ ## back to /home
+ };
+ ## back to the root
+
+B<Notice:> Once you have a prefix set, do not add a caret to the regex:
+
+ prefix '/foo';
+ get qr{^/bar} => sub { ... } # BAD BAD BAD
+ get qr{/bar} => sub { ... } # Good!
+
+=head2 del
+
+Defines a route for HTTP B<DELETE> requests to the given URL:
+
+ del '/resource' => sub { ... };
+
+=head2 options
+
+Defines a route for HTTP B<OPTIONS> requests to the given URL:
+
+ options '/resource' => sub { ... };
+
+=head2 put
+
+Defines a route for HTTP B<PUT> requests to the given URL:
+
+ put '/resource' => sub { ... };
+
+=head2 redirect
+
+Generates a HTTP redirect (302). You can either redirect to a complete
+different site or within the application:
+
+ get '/twitter', sub {
+ redirect 'http://twitter.com/me';
+ # Any code after the redirect will not be executed.
+ };
+
+B<WARNING:> Issuing a C<redirect> immediately exits the current route.
+Thus, any code after a C<redirect> is ignored, until the end of the route.
+Hence, it's not necessary anymore to use C<return> with C<redirect>.
+
+You can also force Dancer to return a specific 300-ish HTTP response code:
+
+ get '/old/:resource', sub {
+ redirect '/new/' . route_parameters->get('resource'), 301;
+ };
+
+=head2 request
+
+Returns a L<Dancer2::Core::Request> object representing the current request.
+
+See the L<Dancer2::Core::Request> documentation for the methods you can
+call, for example:
+
+ request->referer; # value of the HTTP referer header
+ request->remote_address; # user's IP address
+ request->user_agent; # User-Agent header value
+
+=head2 request_header
+
+Returns request header(s).
+
+ get '/get/headers' => sub {
+ my $xfoo = request_header 'X-Foo';
+ ...
+ };
+
+=head2 send_as
+
+Allows the current route handler to return specific content types to the
+client using either a specified serializer or as html.
+
+Any Dancer2 serializer may be used. The specified serializer class will
+be loaded if required, or an error generated if the class can not be found.
+Serializer configuration may be added to your apps C<engines> configuration.
+
+If C<html> is specified, the content will be returned assuming it is HTML with
+appropriate C<Content-Type> headers and encoded using the apps configured
+C<charset> (or UTF-8).
+
+ set serializer => 'YAML';
+ set template => 'TemplateToolkit';
+
+ # returns html (not YAML)
+ get '/' => sub { send_as html => template 'welcome.tt' };
+
+ # return json (not YAML)
+ get '/json' => sub {
+ send_as JSON => [ some => { data => 'structure' } ];
+ };
+
+C<send_as> uses L</send_file> to return the content immediately. You may
+pass any option C<send_file> supports as an extra option. For example:
+
+ # return json with a custom content_type header
+ get '/json' => sub {
+ send_as JSON => [ some => { data => 'structure' } ],
+ { content_type => 'application/json; charset=UTF-8' },
+ };
+
+B<WARNING:> Issuing a send_as immediately exits the current route, and
+performs the C<send_as>. Thus, any code after a C<send_as> is ignored,
+until the end of the route. Hence, it's not necessary to use C<return>
+with C<send_as>.
+
+ get '/some/route' => sub {
+ if (...) {
+ send_as JSON => $some_data;
+
+ # this code will be ignored
+ do_stuff();
+ }
+ };
+
+=head2 send_error
+
+Returns a HTTP error. By default the HTTP code returned is 500:
+
+ get '/photo/:id' => sub {
+ if (...) {
+ send_error("Not allowed", 403);
+ } else {
+ # return content
+ }
+ }
+
+B<WARNING:> Issuing a send_error immediately exits the current route, and
+performs the C<send_error>. Thus, any code after a C<send_error> is ignored,
+until the end of the route. Hence, it's not necessary anymore to use C<return>
+with C<send_error>.
+
+ get '/some/route' => sub {
+ if (...) {
+ # Something bad happened, stop immediately!
+ send_error(..);
+
+ # this code will be ignored
+ do_stuff();
+ }
+ };
+
+=head2 send_file
+
+Lets the current route handler send a file to the client. Note that the path
+of the file must be relative to the B<public> directory unless you use the
+C<system_path> option (see below).
+
+ get '/download/:file' => sub {
+ return send_file(route_parameters->get('file'));
+ }
+
+B<WARNING:> Issuing a C<send_file> immediately exits the current route, and
+performs the C<send_file>. Thus, any code after a C<send_file> is ignored,
+until the end of the route. Hence, it's not necessary anymore to use C<return>
+with C<send_file>.
+
+ get '/some/route' => sub {
+ if (...) {
+ # OK, send her what she wants...
+ send_file(...);
+
+ # this code will be ignored
+ do_stuff();
+ }
+ };
+
+C<send_file> will use PSGI streaming if the server supports it (most, if
+not all, do). You can explicitly disable streaming by passing
+C<streaming =E<gt> 0> as an option to C<send_file>.
+
+ get '/download/:file' => sub {
+ send_file( route_parameters->get('file'), streaming => 0 );
+ }
+
+The content-type will be set depending on the current MIME types definition
+(see C<mime> if you want to define your own).
+
+If your filename does not have an extension, you are passing in a filehandle,
+or you need to force a specific mime type, you can pass it to C<send_file>
+as follows:
+
+ send_file(route_parameters->get('file'), content_type => 'image/png');
+ send_file($fh, content_type => 'image/png');
+
+Also, you can use your aliases or file extension names on C<content_type>,
+like this:
+
+ send_file(route_parameters->get('file'), content_type => 'png');
+
+The encoding of the file or filehandle may be specified by passing both
+the C<content_type> and C<charset> options. For example:
+
+ send_file($fh, content_type => 'text/csv', charset => 'utf-8' );
+
+For files outside your B<public> folder, you can use the C<system_path>
+switch. Just bear in mind that its use needs caution as it can be dangerous.
+
+ send_file('/etc/passwd', system_path => 1);
+
+If you have your data in a scalar variable, C<send_file> can be useful as
+well. Pass a reference to that scalar, and C<send_file> will behave as if
+there was a file with that contents:
+
+ send_file( \$data, content_type => 'image/png' );
+
+Note that Dancer is unable to guess the content type from the data contents.
+Therefore you might need to set the C<content_type> properly. For this kind
+of usage an attribute named C<filename> can be useful. It is used as the
+Content-Disposition header, to hint the browser about the filename it should
+use.
+
+ send_file( \$data, content_type => 'image/png'
+ filename => 'onion.png' );
+
+By default the Content-Disposition header uses the "attachment" type, which
+triggers a "Save" dialog in some browsers. Supply a C<content_disposition>
+attribute of "inline" to have the file displayed inline by the browser.
+
+=head2 set
+
+Defines a setting:
+
+ set something => 'value';
+
+You can set more than one value at once:
+
+ set something => 'value', otherthing => 'othervalue';
+
+=head2 setting
+
+Returns the value of a given setting:
+
+ setting('something'); # 'value'
+
+=head2 session
+
+Provides access to all data stored in the user's session (if any).
+
+It can also be used as a setter to store data in the session:
+
+ # getter example
+ get '/user' => sub {
+ if (session('user')) {
+ return "Hello, ".session('user')->name;
+ }
+ };
+
+ # setter example
+ post '/user/login' => sub {
+ ...
+ if ($logged_in) {
+ session user => $user;
+ }
+ ...
+ };
+
+You may also need to clear a session:
+
+ # destroy session
+ get '/logout' => sub {
+ ...
+ app->destroy_session;
+ ...
+ };
+
+If you need to fetch the session ID being used for any reason:
+
+ my $id = session->id;
+
+=head2 splat
+
+Returns the list of captures made from a route handler with a route pattern
+which includes wildcards:
+
+ get '/file/*.*' => sub {
+ my ($file, $extension) = splat;
+ ...
+ };
+
+There is also the extensive splat (A.K.A. "megasplat"), which allows
+extensive greedier matching, available using two asterisks. The additional
+path is broken down and returned as an arrayref:
+
+ get '/entry/*/tags/**' => sub {
+ my ( $entry_id, $tags ) = splat;
+ my @tags = @{$tags};
+ };
+
+The C<splat> keyword in the above example for the route F</entry/1/tags/one/two>
+would set C<$entry_id> to C<1> and C<$tags> to C<['one', 'two']>.
+
+=head2 start
+
+Starts the application or the standalone server (depending on the deployment
+choices).
+
+This keyword should be called at the very end of the script, once all routes
+are defined. At this point, Dancer2 takes over.
+
+=head2 to_app
+
+Returns the PSGI coderef for the current (and only the current) application.
+
+You can call it as a method on the class or as a DSL:
+
+ my $app = MyApp->to_app;
+
+ # or
+
+ my $app = to_app;
+
+There is a
+L<Dancer Advent Calendar article|http://advent.perldancer.org/2014/9> covering
+this keyword and its usage further.
+
+=head2 psgi_app
+
+Provides the same functionality as C<to_app> but uses the deprecated
+Dispatcher engine. You should use C<to_app> instead.
+
+=head2 status
+
+Changes the status code provided by an action. By default, an action will
+produce an C<HTTP 200 OK> status code, meaning everything is OK:
+
+ get '/download/:file' => {
+ if (! -f route_parameters->get('file')) {
+ status 'not_found';
+ return "File does not exist, unable to download";
+ }
+ # serving the file...
+ };
+
+In that example, Dancer will notice that the status has changed, and will
+render the response accordingly.
+
+The C<status> keyword receives either a numeric status code or its name in
+lower case, with underscores as a separator for blanks - see the list in
+L<Dancer2::Core::HTTP/"HTTP CODES">. As an example, The above call translates
+to setting the code to C<404>.
+
+=head2 template
+
+Returns the response of processing the given template with the given
+parameters (and optional settings), wrapping it in the default or specified
+layout too, if layouts are in use.
+
+An example of a route handler which returns the result of using template to
+build a response with the current template engine:
+
+ get '/' => sub {
+ ...
+ return template 'some_view', { token => 'value'};
+ };
+
+Note that C<template> simply returns the content, so when you use it in a
+route handler, if execution of the route handler should stop at that point,
+make sure you use C<return> to ensure your route handler returns the content.
+
+Since C<template> just returns the result of rendering the template, you can
+also use it to perform other templating tasks, e.g. generating emails:
+
+ post '/some/route' => sub {
+ if (...) {
+ email {
+ to => 'someone@example.com',
+ from => 'foo@example.com',
+ subject => 'Hello there',
+ msg => template('emails/foo', { name => body_parameters->get('name') }),
+ };
+
+ return template 'message_sent';
+ } else {
+ return template 'error';
+ }
+ };
+
+Compatibility notice: C<template> was changed in version 1.3090 to
+immediately interrupt execution of a route handler and return the content,
+as it's typically used at the end of a route handler to return content.
+However, this caused issues for some people who were using C<template> to
+generate emails etc, rather than accessing the template engine directly, so
+this change has been reverted in 1.3091.
+
+The first parameter should be a template available in the views directory,
+the second one (optional) is a hashref of tokens to interpolate, and the
+third (again optional) is a hashref of options.
+
+For example, to disable the layout for a specific request:
+
+ get '/' => sub {
+ template 'index', {}, { layout => undef };
+ };
+
+Or to request a specific layout, of course:
+
+ get '/user' => sub {
+ template 'user', {}, { layout => 'user' };
+ };
+
+Some tokens are automatically added to your template (C<perl_version>,
+C<dancer_version>, C<settings>, C<request>, C<vars> and, if you
+have sessions enabled, C<session>). Check L<Default Template
+Variables|Dancer2::Manual/Default-Template-Variables>
+for further details.
+
+=head2 to_dumper ($structure)
+
+Serializes a structure with Data::Dumper.
+
+Calling this function will B<not> trigger the serialization's hooks.
+
+=head2 to_json ($structure, \%options)
+
+Serializes a structure to JSON. You should probably use C<encode_json> instead
+which handles encoding the result for you.
+
+=head2 to_yaml ($structure)
+
+Serializes a structure to YAML.
+
+Calling this function will B<not> trigger the serialization's hooks.
+
+=head2 true
+
+Constant that returns a true value (1).
+
+=head2 upload
+
+Provides access to file uploads. Any uploaded file is accessible as a
+L<Dancer2::Core::Request::Upload> object. You can access all parsed uploads
+via:
+
+ post '/some/route' => sub {
+ my $file = upload('file_input_foo');
+ # $file is a Dancer2::Core::Request::Upload object
+ };
+
+If you named multiple inputs of type "file" with the same name, the C<upload>
+keyword would return an Array of L<Dancer2::Core::Request::Upload> objects:
+
+ post '/some/route' => sub {
+ my ($file1, $file2) = upload('files_input');
+ # $file1 and $file2 are Dancer2::Core::Request::Upload objects
+ };
+
+You can also access the raw hashref of parsed uploads via the current
+C<request> object:
+
+ post '/some/route' => sub {
+ my $all_uploads = request->uploads;
+ # $all_uploads->{'file_input_foo'} is a Dancer2::Core::Request::Upload object
+ # $all_uploads->{'files_input'} is an arrayref of Dancer2::Core::Request::Upload objects
+ };
+
+Note that you can also access the filename of the upload received via the
+C<body_parameters> keyword:
+
+ post '/some/route' => sub {
+ # body_parameters->get('files_input') is the filename of the file uploaded
+ };
+
+See L<Dancer2::Core::Request::Upload> for details about the interface provided.
+
+=head2 uri_for
+
+Returns a fully-qualified URI for the given path:
+
+ get '/' => sub {
+ redirect uri_for('/path');
+ # can be something like: http://localhost:5000/path
+ };
+
+Query string parameters can be provided by passing a hashref as a second param:
+
+ uri_for('/path', { foo => 'bar' });
+ # would return e.g. http://localhost:5000/path?foo=bar
+
+By default, the parameters will be URL encoded:
+
+ uri_for('/path', { foo => 'hope;faith' });
+ # would return http://localhost:5000/path?foo=hope%3Bfaith
+
+If desired (for example, if you've already encoded your query
+parameters and you want to prevent double encoding) you can disable
+URL encoding via a third parameter:
+
+ uri_for('/path', { foo => 'qux%3Dquo' }, 1);
+ # would return http://localhost:5000/path?foo=qux%3Dquo
+
+=head2 captures
+
+Returns a reference to a copy of C<%+>, if there are named captures in the
+route's regular expression.
+
+Named captures are a feature of Perl 5.10, and are not supported in earlier
+versions:
+
+ get qr{
+ / (?<object> user | ticket | comment )
+ / (?<action> delete | find )
+ / (?<id> \d+ )
+ /?$
+ }x
+ , sub {
+ my $value_for = captures;
+ "i don't want to $$value_for{action} the $$value_for{object} $$value_for{id} !"
+ };
+
+=head2 var
+
+Provides an accessor for variables shared between hooks and route
+handlers. Given a key/value pair, it sets a variable:
+
+ hook before => sub {
+ var foo => 42;
+ };
+
+Later, route handlers and other hooks will be able to read that variable:
+
+ get '/path' => sub {
+ my $foo = var 'foo';
+ ...
+ };
+
+=head2 vars
+
+Returns the hashref of all shared variables set during the hook/route
+chain with the C<var> keyword:
+
+ get '/path' => sub {
+ if (vars->{foo} eq 42) {
+ ...
+ }
+ };
+
+=head2 warning
+
+Logs a warning message through the current logger engine:
+
+ warning "This is a warning";
+
+See L<Dancer2::Core::Role::Logger> for details on how to configure where log
+messages go.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Manual/Deployment.pod b/lib/Dancer2/Manual/Deployment.pod
new file mode 100644
index 00000000..ee88388e
--- /dev/null
+++ b/lib/Dancer2/Manual/Deployment.pod
@@ -0,0 +1,732 @@
+# PODNAME: Dancer2::Manual::Deployment
+# ABSTRACT: common ways to put your Dancer app into use
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Manual::Deployment - common ways to put your Dancer app into use
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Dancer has been designed to be flexible, and this flexibility extends to your
+choices when deploying your Dancer app.
+
+=head2 Running stand-alone
+
+To start your application, just run plackup:
+
+ $ plackup bin/app.psgi
+ HTTP::Server::PSGI: Accepting connections at http://0:5000/
+
+Point your browser at it, and away you go!
+
+This option can be useful for small personal web apps or internal apps, but if
+you want to make your app available to the world, it probably won't suit you.
+
+=head3 Auto Reloading the Application
+
+While developing your application, it is often handy to have the server
+automatically reload your application when changes are made. There are
+two recommended ways of handling this with Dancer: using C< plackup -r >
+and L<Plack::Loader::Shotgun>. Both have their advantages and disadvantages
+(which will be explained below).
+
+Regardless of the method you use, it is B< not > recommended that you
+automatically reload your applications in a production environment, for
+reasons of performance, deployment best practices, etc.
+
+For Dancer 1 programmers that used the C< auto_reload > option, please use
+one of these alternatives instead:
+
+=head4 Auto reloading with C< plackup -r >
+
+Plack's built-in reloader will reload your application anytime a file in
+your application's directory (usually, F< /bin >) changes. You will likely
+want to monitor your F< lib/ > directory too, using the C< -R > option:
+
+ $ plackup -r -R lib bin/app.psgi
+
+There is a performance hit associated with this, as Plack will spin off
+a separate process that monitors files in the application and other
+specified directories. If the timestamp of any files in a watched
+directory changes, the application is recompiled and reloaded.
+
+See the L<plackup> docs for more information on the C< -r > and C< -R >
+options.
+
+=head4 Auto reloading with plackup and Shotgun
+
+There may be circumstances where Plack's built-in reloader won't work for
+you, be it for the way it looks for changes, or because there are many
+directories you need to monitor, or you want to reload the application any
+time one of the modules in Perl's F< lib/ > path changes.
+L<Plack::Loader::Shotgun> makes this easy by recompiling the application
+on every request.
+
+To use Shotgun, specify it using the loader argument to C< plackup (-L) >:
+
+ $ plackup -L Shotgun bin/app.psgi
+
+The Shotgun, while effective, can quickly cause you performance issues, even
+during the development phase of your application. As the number of plugins
+you use in your application grows, as the number of static resources (images,
+etc.) grows, the more requests your server process needs to handle. Since
+each request recompiles the application, even simple page refreshes can get
+unbearably slow over time. Use with caution.
+
+You can bypass Shotgun's auto-reloading of specific modules with the
+C< -M > switch:
+
+ $ plackup -L Shotgun -M<MyApp::Foo> -M<MyApp::Bar> bin/app.psgi
+
+On Windows, Shotgun loader is known to cause huge memory leaks in a
+fork-emulation layer. If you are aware of this and still want to run the
+loader, please use the following command:
+
+ > set PLACK_SHOTGUN_MEMORY_LEAK=1 && plackup -L Shotgun bin\app.psgi
+ HTTP::Server::PSGI: Accepting connections at http://0:5000/
+
+B<Please note: > if you are using Dancer 2's asynchronous capabilities, using
+Shotgun will kill Twiggy. If you need async processing, consider an
+alternative to Shotgun.
+
+=head2 Running under Apache
+
+You can run your Dancer app from Apache using the following examples:
+
+=head3 As a CGI script
+
+In its simplest form, your Dancer app can be run as a simple CGI script
+out-of-the-box. You will need to enable the Apache mod_cgi or mod_cgid modules
+(C<a2enmod cgi> or C<a2enmod cgid> on Debian-based systems) and mod_rewrite
+(C<a2enmod rewrite>). The Perl module L<Plack::Runner> is required.
+
+The following is an example apache configuration. Depending on your Apache
+configuration layout, this should be placed in C<httpd.conf> or
+C<sites-available/*site*>. The configuration options can also be placed in
+C<.htaccess> files if you prefer.
+
+ <VirtualHost *:80>
+ ServerName www.example.com
+
+ # /srv/www.example.com is the root of your
+ # dancer application
+ DocumentRoot /srv/www.example.com/public
+
+ ServerAdmin you@example.com
+
+ <Directory "/srv/www.example.com/public">
+ AllowOverride None
+ Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
+ AddHandler cgi-script .cgi
+ # Apache 2.2
+ Order allow,deny
+ Allow from all
+ # Apache 2.4
+ Require all granted
+ </Directory>
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^(.*)$ /dispatch.cgi$1 [QSA,L]
+
+ ErrorLog /var/log/apache2/www.example.com-error.log
+ CustomLog /var/log/apache2/www.example.com-access_log common
+ </VirtualHost>
+
+Now you can access your dancer application URLs as if you were using the
+embedded web server.
+
+ http://www.example.com/
+
+This option is a no-brainer, easy to setup and low maintenance, but serves
+requests slower than all other options, as each time a request is made to your
+server, Apache will start your application. This might be suitable for a small,
+occasionally-used sites, as the application is not using resources when it is
+not being accessed. For anything more, you probably want to use FastCGI instead
+(see next section).
+
+To list all currently loaded modules, type C<apachectl -M>
+(C<apache2ctl -M> on Debian/Ubuntu).
+
+=head3 As a FastCGI script
+
+This has all the easy-to-setup and low-maintenance advantages of CGI, but is
+much faster for each request, as it keeps a copy of the application running all
+the time.
+
+You will still need to enable C<mod_rewrite>, but will need to use a FastCGI
+module instead of a CGI module. There are 3 available:
+L<mod_fcgid|http://httpd.apache.org/mod_fcgid/>,
+L<mod_fastcgi|http://www.fastcgi.com/> and
+L<mod_proxy_fcgi|https://httpd.apache.org/docs/trunk/mod/mod_proxy_fcgi.html>.
+For this example, we will use mod_fastcgi (C<a2enmod fastcgi> in Debian).
+
+The CGI configuration above now changes as follows (differences highlighted
+with XXX):
+
+ <VirtualHost *:80>
+ ServerName www.example.com
+
+ # /srv/www.example.com is the root of your
+ # dancer application
+ DocumentRoot /srv/www.example.com/public
+
+ ServerAdmin you@example.com
+
+ # XXX Start a FastCGI server to run in the background
+ FastCgiServer /srv/www.example.com/public/dispatch.fcgi
+
+ <Directory "/srv/www.example.com/public">
+ AllowOverride None
+ Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
+ # XXX Use FastCGI handler instead of CGI
+ AddHandler fastcgi-script .fcgi
+ # Apache 2.2
+ Order allow,deny
+ Allow from all
+ # Apache 2.4
+ Require all granted
+ </Directory>
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_FILENAME} !-f
+ # Run FastCGI dispatcher instead of CGI dispatcher
+ RewriteRule ^(.*)$ /dispatch.fcgi$1 [QSA,L]
+
+ ErrorLog /var/log/apache2/www.example.com-error.log
+ CustomLog /var/log/apache2/www.example.com-access_log common
+ </VirtualHost>
+
+This is the easiest way to get a production server up and running, as there is
+no need to worry about daemonizing your application. Apache manages all that
+for you.
+
+=head4 Reloading your application
+
+You can use C<apache2ctl restart> or C<apache2ctl graceful> to reload your
+application. The latter will be more friendly to your users in a production
+environment. If your application loads relatively quickly, then it should go
+unnoticed.
+
+=head4 Configuration
+
+See L<http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html> for FastCGI
+configuration options. An example configuration:
+
+ FastCgiServer /srv/www.example.com/public/dispatch.fcgi -processes 5 -initial-env DANCER_ENVIRONMENT="production"
+
+=head3 With Plack
+
+You can run your app from Apache using PSGI (Plack), with a config like the
+following:
+
+ <VirtualHost myapp.example.com>
+ ServerName www.myapp.example.com
+ ServerAlias myapp.example.com
+ DocumentRoot /websites/myapp.example.com
+
+ <Directory /home/myapp/myapp>
+ AllowOverride None
+ Order allow,deny
+ Allow from all
+ </Directory>
+
+ <Location />
+ SetHandler perl-script
+ PerlResponseHandler Plack::Handler::Apache2
+ PerlSetVar psgi_app /websites/myapp.example.com/app.psgi
+ </Location>
+
+ ErrorLog /websites/myapp.example.com/logs/error_log
+ CustomLog /websites/myapp.example.com/logs/access_log common
+ </VirtualHost>
+
+To set the environment you want to use for your application (production or
+development), you can set it this way:
+
+ <VirtualHost>
+ ...
+ SetEnv DANCER_ENVIRONMENT "production"
+ ...
+ </VirtualHost>
+
+=head3 Running multiple applications under the same virtualhost
+
+If you want to deploy multiple applications under the same C<VirtualHost>
+(using one application per directory, for example) you can use the following
+example Apache configuration.
+
+This example uses the FastCGI dispatcher that comes with Dancer, but you should
+be able to adapt this to use any other way of deployment described in this
+guide. The only purpose of this example is to show how to deploy multiple
+applications under the same base directory/virtualhost.
+
+ <VirtualHost *:80>
+ ServerName localhost
+ DocumentRoot "/path/to/rootdir"
+ RewriteEngine On
+ RewriteCond %{REQUEST_FILENAME} !-f
+
+ <Directory "/path/to/rootdir">
+ AllowOverride None
+ Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
+ Order allow,deny
+ Allow from all
+ AddHandler fastcgi-script .fcgi
+ </Directory>
+
+ RewriteRule /App1(.*)$ /App1/public/dispatch.fcgi$1 [QSA,L]
+ RewriteRule /App2(.*)$ /App2/public/dispatch.fcgi$1 [QSA,L]
+ ...
+ RewriteRule /AppN(.*)$ /AppN/public/dispatch.fcgi$1 [QSA,L]
+ </VirtualHost>
+
+Of course, if your Apache configuration allows that, you can put the
+RewriteRules in a .htaccess file directly within the application's directory,
+which lets you add a new application without changing the Apache configuration.
+
+=head2 Running on PSGI-based Perl webservers
+
+A number of Perl web servers supporting PSGI are available on cpan:
+
+=over 4
+
+=item L<Starman>
+
+C<Starman> is a high performance web server, with support for preforking,
+signals, multiple interfaces, graceful restarts and dynamic worker pool
+configuration.
+
+=item L<Twiggy>
+
+C<Twiggy> is an C<AnyEvent> web server, it's light and fast.
+
+=item L<Corona>
+
+C<Corona> is a C<Coro> based web server.
+
+=back
+
+Similar to running standalone, use plackup to start your application
+(see L<Plack> and specific servers above for all available options):
+
+ $ plackup bin/app.psgi
+ $ plackup -E deployment -s Starman --workers=10 -p 5001 -a bin/app.psgi
+
+As you can see, the scaffolded Perl script for your app can be used as a PSGI
+startup file.
+
+=head3 Enabling content compression
+
+Content compression (gzip, deflate) can be easily enabled via a Plack
+middleware (see L<Plack/Plack::Middleware>): L<Plack::Middleware::Deflater>.
+It's a middleware to encode the response body in gzip or deflate, based on
+the C<Accept-Encoding> HTTP request header.
+
+Enable it as you would enable any Plack middleware. First you need to
+install L<Plack::Middleware::Deflater>, then in the handler (usually
+F<app.psgi>) edit it to use L<Plack::Builder>, as described above:
+
+ use Dancer2;
+ use MyWebApp;
+ use Plack::Builder;
+
+ builder {
+ enable 'Deflater';
+ dance;
+ };
+
+To test if content compression works, trace the HTTP request and response
+before and after enabling this middleware. Among other things, you should
+notice that the response is gzip or deflate encoded, and contains a header
+C<Content-Encoding> set to C<gzip> or C<deflate>.
+
+=head3 Creating a service
+
+You can turn your app into proper service running in background using one of
+the following examples:
+
+=head4 Using Ubic
+
+L<Ubic> is an extensible perlish service manager. You can use it to start
+and stop any services, automatically start them on reboots or daemon
+failures, and implement custom status checks.
+
+A basic PSGI service description (usually in C</etc/ubic/service/application>):
+
+ use parent qw(Ubic::Service::Plack);
+
+ # if your application is not installed in @INC path:
+ sub start {
+ my $self = shift;
+ $ENV{PERL5LIB} = '/path/to/your/application/lib';
+ $self->SUPER::start(@_);
+ }
+
+ __PACKAGE__->new(
+ server => 'Starman',
+ app => '/path/to/your/application/app.psgi',
+ port => 5000,
+ user => 'www-data',
+ );
+
+Run C<ubic start application> to start the service.
+
+=head4 Using daemontools
+
+daemontools is a collection of tools for managing UNIX services. You can use
+it to easily start/restart/stop services.
+
+A basic script to start an application: (in C</service/application/run>)
+
+ #!/bin/sh
+
+ # if your application is not installed in @INC path:
+ export PERL5LIB='/path/to/your/application/lib'
+
+ exec 2>&1 \
+ /usr/local/bin/plackup -s Starman -a /path/to/your/application/app.psgi -p 5000
+
+=head2 Running stand-alone behind a proxy / load balancer
+
+Another option would be to run your app stand-alone as described above, but then
+use a proxy or load balancer to accept incoming requests (on the standard port
+80, say) and feed them to your Dancer app. Also, in this case you might want
+to look at the C<behind_proxy> configuration option, to make sure that all the
+URLs are constructed properly.
+
+ behind_proxy: 1
+
+This setup can be achieved using various software; examples would include:
+
+=head3 Using Apache's mod_proxy
+
+You could set up a C<VirtualHost> for your web app, and proxy all requests through
+to it:
+
+ <VirtualHost mywebapp.example.com:80>
+ ProxyPass / http://localhost:3000/
+ ProxyPassReverse / http://localhost:3000/
+ </VirtualHost>
+
+Or, if you want your webapp to share an existing VirtualHost, you could have
+it under a specified dir:
+
+ ProxyPass /mywebapp/ http://localhost:3000/
+ ProxyPassReverse /mywebapp/ http://localhost:3000/
+
+It is important for you to note that the Apache2 modules C<mod_proxy> and
+C<mod_proxy_http> must be enabled:
+
+ $ a2enmod proxy
+ $ a2enmod proxy_http
+
+It is also important to set permissions for proxying for security purposes,
+below is an example.
+
+ <Proxy *>
+ Order allow,deny
+ Allow from all
+ </Proxy>
+
+=head3 Using perlbal
+
+C<Perlbal> is a single-threaded event-based server written in Perl supporting
+HTTP load balancing, web serving, and a mix of the two, available from
+L<http://www.danga.com/perlbal/>
+
+It processes hundreds of millions of requests a day just for LiveJournal, Vox
+and TypePad and dozens of other "Web 2.0" applications.
+
+It can also provide a management interface to let you see various information on
+requests handled etc.
+
+It could easily be used to handle requests for your Dancer apps, too.
+
+It can be easily installed from CPAN:
+
+ perl -MCPAN -e 'install Perlbal'
+
+Once installed, you'll need to write a configuration file. See the examples
+provided with perlbal, but you'll probably want something like:
+
+ CREATE POOL my_dancers
+ POOL my_dancers ADD 10.0.0.10:3030
+ POOL my_dancers ADD 10.0.0.11:3030
+ POOL my_dancers ADD 10.0.0.12:3030
+ POOL my_dancers ADD 10.0.0.13:3030
+
+ CREATE SERVICE my_webapp
+ SET listen = 0.0.0.0:80
+ SET role = reverse_proxy
+ SET pool = my_dancers
+ SET persist_client = on
+ SET persist_backend = on
+ SET verify_backend = on
+ ENABLE my_webapp
+
+=head3 Using balance
+
+C<balance> is a simple load-balancer from Inlab Software, available from
+L<http://www.inlab.de/balance.html>.
+
+It could be used simply to hand requests to a standalone Dancer app. You could
+even run several instances of your Dancer app, on the same machine or on several
+machines, and use a machine running C<balance> to distribute the requests between
+them, for some serious heavy traffic handling!
+
+To listen on port 80, and send requests to a Dancer app on port 3000:
+
+ balance http localhost:3000
+
+To listen on a specified IP only on port 80, and distribute requests between
+multiple Dancer apps on multiple other machines:
+
+ balance -b 10.0.0.1 80 10.0.0.2:3000 10.0.0.3:3000 10.0.0.4:3000
+
+=head3 Using Lighttpd
+
+You can use Lighttp's C<mod_proxy>:
+
+ $HTTP["url"] =~ "/application" {
+ proxy.server = (
+ "/" => (
+ "application" => ( "host" => "127.0.0.1", "port" => 3000 )
+ )
+ )
+ }
+
+This configuration will proxy all request to the B</application> path to the
+path B</> on localhost:3000.
+
+=head3 Using Nginx
+
+with Nginx:
+
+ upstream backendurl {
+ server unix:THE_PATH_OF_YOUR_PLACKUP_SOCKET_HERE.sock;
+ }
+
+ server {
+ listen 80;
+ server_name YOUR_HOST_HERE;
+
+ access_log /var/log/YOUR_ACCESS_LOG_HERE.log;
+ error_log /var/log/YOUR_ERROR_LOG_HERE.log info;
+
+ root YOUR_ROOT_PROJECT/public;
+ location / {
+ try_files $uri @proxy;
+ access_log off;
+ expires max;
+ }
+
+ location @proxy {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_pass http://backendurl;
+ }
+
+ }
+
+You will need plackup to start a worker listening on a socket :
+
+ cd YOUR_PROJECT_PATH
+ sudo -u www plackup -E production -s Starman --workers=2 -l THE_PATH_OF_YOUR_PLACKUP_SOCKET_HERE.sock -a bin/app.pl
+
+A good way to start this is to use C<daemontools> and place this line with
+all environments variables in the "run" file.
+
+=head3 Using HAProxy
+
+C<HAProxy> is a reliable high-performance TCP/HTTP load balancer written in C available from
+L<https://www.haproxy.org/>.
+
+Suppose we want to run an application at C<app.example.com:80> and would to use two
+backends listen on hosts C<app-be1.example.com:3000> and C<app-be2.example.com:3000>.
+
+Here is HAProxy configuration file (haproxy.conf):
+
+ global
+ nbproc 1
+ maxconn 4096
+ user nobody
+ group nobody
+ # haproxy logs will be collected by syslog
+ # syslog: unix socket path or tcp pair (ipaddress:port)
+ log /var/run/log local0
+ daemon
+ # enable compression (haproxy v1.5-dev13 and above required)
+ tune.comp.maxlevel 5
+
+ defaults
+ log global
+ option httpclose
+ option httplog
+ option dontlognull
+ option forwardfor
+ option abortonclose
+ mode http
+ balance roundrobin
+ retries 3
+ timeout connect 5s
+ timeout server 30s
+ timeout client 30s
+ timeout http-keep-alive 200m
+ # enable compression (haproxy v1.5-dev13 and above required)
+ compression algo gzip
+ compression type text/html application/javascript text/css application/x-javascript text/javascript
+
+ # application frontend (available at http://app.example.com)
+ frontend app.example.com
+ bind :80
+ # modify request headers
+ reqadd X-Forwarded-Proto:\ http
+ reqadd X-Forwarded-Port:\ 80
+ # modify response headers
+ rspdel ^Server:.*
+ rspdel ^X-Powered-By:.*
+ rspadd Server:\ Dethklok\ (Unix/0.2.3)
+ rate-limit sessions 1024
+ acl is-haproxy-stats path_beg /stats
+ # uncomment if you'd like to get haproxy usage statistics
+ # use_backend haproxy if is-haproxy-stats
+ default_backend dynamic
+
+ # haproxy statistics (available at http://app.example.com/stats)
+ backend haproxy
+ stats uri /stats
+ stats refresh 180s
+ stats realm app.example.com\ haproxy\ statistics
+ # change credentials
+ stats auth admin1:password1
+ stats auth admin2:password2
+ stats hide-version
+ stats show-legends
+
+ # application backends
+ backend dynamic
+ # change path_info to check and value of the Host header sent to application server
+ option httpchk HEAD / HTTP/1.1\r\nHost:\ app.example.com
+ server app1 app-be1.example.com:3000 check inter 30s
+ server app2 app-be2.example.com:3000 check inter 30s
+
+We will need to start the workers on each backend of our application. This can be done by starman utility:
+
+ # on app-be1.example.com
+ $ starman --workers=2 --listen :3000 /path/to/app.pl
+ # on app-be2.example.com
+ $ starman --workers=2 --listen :3000 /path/to/app.pl
+
+Then start the haproxy itself:
+
+ # check the configuration..
+ $ sudo haproxy -c -f haproxy.conf
+ # now really start it..
+ $ sudo haproxy -f haproxy.conf
+
+=head2 Running on lighttpd
+
+=head3 Running on lighttpd (CGI)
+
+To run as a CGI app on lighttpd, just create a soft link to the C<dispatch.cgi>
+script (created when you run C<dancer -a MyApp>) inside your system's C<cgi-bin>
+folder. Make sure C<mod_cgi> is enabled.
+
+ ln -s /path/to/MyApp/public/dispatch.cgi /usr/lib/cgi-bin/mycoolapp.cgi
+
+=head3 Running on lighttpd (FastCGI)
+
+Make sure C<mod_fcgi> is enabled. You also must have L<FCGI> installed.
+
+This example configuration uses TCP/IP:
+
+ $HTTP["url"] == "^/app" {
+ fastcgi.server += (
+ "/app" => (
+ "" => (
+ "host" => "127.0.0.1",
+ "port" => "5000",
+ "check-local" => "disable",
+ )
+ )
+ )
+ }
+
+Launch your application:
+
+ plackup -s FCGI --port 5000 bin/app.psgi
+
+This example configuration uses a socket:
+
+ $HTTP["url"] =~ "^/app" {
+ fastcgi.server += (
+ "/app" => (
+ "" => (
+ "socket" => "/tmp/fcgi.sock",
+ "check-local" => "disable",
+ )
+ )
+ )
+ }
+
+Launch your application:
+
+ plackup -s FCGI --listen /tmp/fcgi.sock bin/app.psgi
+
+=head2 Performance Improvements
+
+The following modules can be used to speed up an app in Dancer2:
+
+=over 4
+
+=item * L<URL::Encode::XS>
+
+=item * L<CGI::Deurl::XS>
+
+=item * L<HTTP::Parser::XS>
+
+=item * L<HTTP::XSCookies>
+
+=item * L<Scope::Upper>
+
+=item * L<Type::Tiny::XS>
+
+=back
+
+They would need to be installed separately. This is because L<Dancer2> does
+not incorporate any C code, but it can get C-code compiled as a module.
+Thus, these modules can be used for speed improvement provided:
+
+=over 4
+
+=item * You have access to a C interpreter
+
+=item * You don't need to fatpack your application
+
+=back
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Manual/Migration.pod b/lib/Dancer2/Manual/Migration.pod
new file mode 100644
index 00000000..dba84ddf
--- /dev/null
+++ b/lib/Dancer2/Manual/Migration.pod
@@ -0,0 +1,606 @@
+package Dancer2::Manual::Migration;
+# ABSTRACT: Migrating from Dancer to Dancer2
+
+use strict;
+use warnings;
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Manual::Migration - Migrating from Dancer to Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 Migration from Dancer 1 to Dancer2
+
+This document covers some changes that users will need to be aware of
+while upgrading from L<Dancer> (version 1) to L<Dancer2>.
+
+=head2 Launcher script
+
+The default launcher script F<bin/app.pl> in L<Dancer> looked like this:
+
+ #!/usr/bin/env perl
+ use Dancer;
+ use MyApp;
+ dance;
+
+In L<Dancer2> it is available as F<bin/app.psgi> and looks like this:
+
+ #!/usr/bin/env perl
+
+ use strict;
+ use warnings;
+ use FindBin;
+ use lib "$FindBin::Bin/../lib";
+
+ use MyApp;
+ MyApp->to_app;
+
+So you need to remove the C<use Dancer;> part, replace the C<dance;> command
+by C<< MyApp->to_app; >> (where MyApp is the name of your application), and
+add the following lines:
+
+ use strict;
+ use warnings;
+ use FindBin;
+ use lib "$FindBin::Bin/../lib";
+
+There is a L<Dancer Advent Calendar|http://advent.perldancer.org> article
+L<< covering the C<to_app> keyword|http://advent.perldancer.org/2014/9 >>
+and its usage.
+
+=head2 Configuration
+
+You specify a different location to the directory used for serving static (public)
+content by setting the C<public_dir> option. In that case, you have to set
+C<static_handler> option also.
+
+=head2 Apps
+
+1. In L<Dancer2>, each module is a B<separate application> with its own
+namespace and variables. You can set the application name in each of your
+L<Dancer2> application modules. Different modules can be tied into the same
+app by setting the application name to the same value.
+
+For example, to set the appname directive explicitly:
+
+C<MyApp>:
+
+ package MyApp;
+ use Dancer2;
+ use MyApp::Admin
+
+ hook before => sub {
+ var db => 'Users';
+ };
+
+ get '/' => sub {...};
+
+ 1;
+
+C<MyApp::Admin>:
+
+ package MyApp::Admin;
+ use Dancer2 appname => 'MyApp';
+
+ # use a lexical prefix so we don't override it globally
+ prefix '/admin' => sub {
+ get '/' => sub {...};
+ };
+
+ 1;
+
+Without the appname directive, C<MyApp::Admin> would not have access
+to variable C<db>. In fact, when accessing C</admin>, the before hook would
+not be executed.
+
+See L<Dancer2::Cookbook|https://metacpan.org/pod/Dancer2::Cookbook#Using-the-prefix-feature-to-split-your-application>
+for details.
+
+2. To speed up an app in Dancer2, install the recommended modules listed in the
+L<Dancer2::Manual::Deployment/"Performance Improvements"> section.
+
+=head2 Request
+
+The request object (L<Dancer2::Core::Request>) is now deferring much of
+its code to L<Plack::Request> to be consistent with the known interface
+to L<PSGI> requests.
+
+Currently the following attributes pass directly to L<Plack::Request>:
+
+C<address>, C<remote_host>, C<protocol>, C<port>, C<method>, C<user>,
+C<request_uri>, C<script_name>, C<content_length>, C<content_type>,
+C<content_encoding>, C<referer>, and C<user_agent>.
+
+If previous attributes returned I<undef> for no value beforehand, they
+will return whatever L<Plack::Request> defines now, which just might be
+an empty list.
+
+For example:
+
+ my %data = (
+ referer => request->referer,
+ user_agent => request->user_agent,
+ );
+
+should be replaced by:
+
+ my %data = (
+ referer => request->referer || '',
+ user_agent => request->user_agent || '',
+ );
+
+=head2 Plugins: plugin_setting
+
+C<plugin_setting> returns the configuration of the plugin. It can only be
+called in C<register> or C<on_plugin_import>.
+
+=head2 Routes
+
+L<Dancer2> requires all routes defined via a string to begin with a leading
+slash C</>.
+
+For example:
+
+ get '0' => sub {
+ return "not gonna fly";
+ };
+
+would return an error. The correct way to write this would be to use
+C<get '/0'>
+
+=head2 Route parameters
+
+The C<params> keyword which provides merged parameters used to allow body
+parameters to override route parameters. Now route parameters take
+precedence over query parameters and body parameters.
+
+We have introduced C<route_parameters> to retrieve parameter values from
+the route matching. Please refer to L<Dancer2::Manual> for more
+information.
+
+=head2 Tests
+
+Dancer2 recommends the use of L<Plack::Test>.
+
+For example:
+
+ use strict;
+ use warnings;
+ use Test::More tests => 2;
+ use Plack::Test;
+ use HTTP::Request::Common;
+
+ {
+ package App::Test; # or whatever you want to call it
+ get '/' => sub { template 'index' };
+ }
+
+ my $test = Plack::Test->create( App::Test->to_app );
+ my $res = $test->request( GET '/' );
+
+ ok( $res->is_success, '[GET /] Successful' );
+ like( $res->content, qr{<title>Test2</title>}, 'Correct title' );
+
+Other modules that could be used for testing are:
+
+=over 4
+
+=item * L<Test::TCP>
+
+=item * L<Test::WWW::Mechanize::PSGI>
+
+=back
+
+=head3 Logs
+
+The C<logger_format> in the Logger role (L<Dancer2::Core::Role::Logger>)
+is now C<log_format>.
+
+C<read_logs> can no longer be used, as with L<Dancer2::Test>. Instead,
+L<Dancer2::Logger::Capture> could be used for testing, to capture all
+logs to an object.
+
+For example:
+
+ use strict;
+ use warnings;
+ use Test::More import => ['!pass'];
+ use Plack::Test;
+ use HTTP::Request::Common;
+ use Ref::Util qw<is_coderef>;
+
+ {
+ package App;
+ use Dancer2;
+
+ set log => 'debug';
+ set logger => 'capture';
+
+ get '/' => sub {
+ debug 'this is my debug message';
+ return 1;
+ };
+ }
+
+ 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;
+
+ my $trap = App->dancer_app->logger_engine->trapper;
+
+ is_deeply $trap->read, [
+ { level => 'debug', message => 'this is my debug message' }
+ ];
+ };
+
+=head2 Exports: Tags
+
+The following tags are not needed in L<Dancer2>:
+
+ use Dancer2 qw(:syntax);
+ use Dancer2 qw(:tests);
+ use Dancer2 qw(:script);
+
+The C<plackup> command should be used instead. It provides a development
+server and reads the configuration options in your command line utilities.
+
+=head2 Engines
+
+=over 4
+
+=item * Engines receive a logging callback
+
+Engines now receive a logging callback named C<log_cb>. Engines can use it
+to log anything in run-time, without having to worry about what logging
+engine is used.
+
+This is provided as a callback because the logger might be changed in
+run-time and we want engines to be able to always reach the current one
+without having a reference back to the core application object.
+
+The logger engine doesn't have the attribute since it is the logger itself.
+
+=item * Engines handle encoding consistently
+
+All engines are now expected to handle encoding on their own. User code
+is expected to be in internal Perl representation.
+
+Therefore, all serializers, for example, should deserialize to the Perl
+representation. Templates, in turn, encode to UTF-8 if requested by the
+user, or by default.
+
+One side-effect of this is that C<from_yaml> will call L<YAML>'s C<Load>
+function with decoded input.
+
+=back
+
+=head3 Templating engine changes
+
+Whereas in Dancer1, the following were equivalent for Template::Toolkit:
+
+ template 'foo/bar'
+ template '/foo/bar'
+
+In Dancer2, when using L<Dancer2::Template::TemplateToolkit>, the version with
+the leading slash will try to locate C</foo/bar> relative to your filesystem
+root, not relative to your Dancer application directory.
+
+The L<Dancer2::Template::Simple> engine is unchanged in this respect.
+
+Whereas in Dancer1, template engines have the methods:
+
+ $template_engine->view('foo.tt')
+ $template_engine->view_exists('foo.tt')
+
+In Dancer2, you should instead write:
+
+ $template_engine->view_pathname('foo.tt')
+ $template_engine->pathname_exists($full_path)
+
+You may not need these unless you are writing a templating engine.
+
+=head3 Serializers
+
+You no longer need to implement the C<loaded> method. It is simply
+unnecessary.
+
+=head3 Sessions
+
+Now the L<Simple|Dancer2::Session::Simple> session engine is turned on
+by default, unless you specify a different one.
+
+=head2 Configuration
+
+=head3 C<public_dir>
+
+You cannot set the public directory with C<setting> now. Instead you
+will need to call C<config>:
+
+ # before
+ setting( 'public_dir', 'new_path/' );
+
+ # after
+ config->{'public_dir'} = 'new_path';
+
+=head3 warnings
+
+The C<warnings> configuration option, along with the environment variable
+C<DANCER_WARNINGS>, have been removed and have no effect whatsoever.
+
+They were added when someone requested to be able to load Dancer without
+the L<warnings> pragma, which it adds, just like L<Moose>, L<Moo>, and
+other modules provide.
+
+If you want this to happen now (which you probably shouldn't be doing),
+you can always control it lexically:
+
+ use Dancer2;
+ no warnings;
+
+You can also use Dancer2 within a narrower scope:
+
+ { use Dancer2 }
+ use strict;
+ # warnings are not turned on
+
+However, having L<warnings> turned it is very recommended.
+
+=head3 server_tokens
+
+The configuration C<server_tokens> has been introduced in the reverse (but
+more sensible, and Plack-compatible) form as C<no_server_tokens>.
+
+C<DANCER_SERVER_TOKENS> changed to C<DANCER_NO_SERVER_TOKENS>.
+
+=head3 engines
+
+If you want to use Template::Toolkit instead of the built-in simple templating
+engine you used to enable the following line in the config.yml file.
+
+ template: "template_toolkit"
+
+That was enough to get started. The start_tag and end_tag it used were the same as in
+the simple template <% and %> respectively.
+
+If you wanted to further customize the Template::Toolkit you could also enable or add
+the following:
+
+ engines:
+ template_toolkit:
+ encoding: 'utf8'
+ start_tag: '[%'
+ end_tag: '%]'
+
+In Dancer 2 you can also enable Template::Toolkit with the same configuration option:
+
+ template: "template_toolkit"
+
+But the default start_tag and end_tag are now [% and %], so if you used the default in Dancer 1
+now you will have to explicitly change the start_tag and end_tag values.
+The configuration also got an extral level of depth. Under the C<engine> key there is a C<template>
+key and the C<template_toolkit> key comes below that. As in this example:
+
+ engines:
+ template:
+ template_toolkit:
+ start_tag: '<%'
+ end_tag: '%>'
+
+In a nutshell, if you used to have
+
+ template: "template_toolkit"
+
+You need to replace it with
+
+ template: "template_toolkit"
+ engines:
+ template:
+ template_toolkit:
+ start_tag: '<%'
+ end_tag: '%>'
+
+=head4 Session engine
+
+The session engine is configured in the C<engine> section.
+
+=over 4
+
+=item * C<session_name> changed to C<cookie_name>.
+
+=item * C<session_domain> changed to C<cookie_domain>.
+
+=item * C<session_expires> changed to C<cookie_duration>.
+
+=item * C<session_secure> changed to C<is_secure>.
+
+=item * C<session_is_http_only> changed to C<is_http_only>.
+
+=back
+
+L<Dancer2> also adds two attributes for session:
+
+=over 4
+
+=item * C<cookie_path>
+The path of the cookie to create for storing the session key. Defaults to "/".
+
+=item * C<session_duration>
+Duration in seconds before sessions should expire, regardless of cookie
+expiration. If set, then SessionFactories should use this to enforce a limit on
+session validity.
+
+=back
+
+See L<Dancer2::Core::Role::SessionFactory> for more detailed documentation
+for these options, or the particular session engine for other supported options.
+
+ session: Simple
+
+ engines:
+ session:
+ Simple:
+ cookie_name: dance.set
+ cookie_duration: '24 hours'
+ is_secure: 1
+ is_http_only: 1
+
+=head3 Plack Middleware
+
+In Dancer1 you could set up Plack Middleware using a C<plack_middlewares> key
+in your C<config.yml> file. Under Dancer2 you will instead need to invoke
+middleware using L<Plack::Builder>, as demonstrated in
+L<Dancer2::Manual::Deployment>.
+
+=head2 Keywords
+
+=head3 Calling Keywords Explicitly
+
+In Dancer1, keywords could be imported individually into a package:
+
+ package MyApp;
+ use Dancer qw< get post params session >;
+
+ get '/foo' { ... };
+
+Any keywords you did't export could be called explicitly:
+
+ package MyApp;
+ use Dancer qw< get post params session >;
+ use List::Util qw< any >;
+
+ Dancer::any sub { ... };
+
+Dancer2's DSL is implemented differently. Keywords only exist in the namespace
+of the package which C<use>s Dancer2, i.e. there is no C<Dancer2::any>, only
+e.g. C<MyApp::any>.
+
+If you only want individual keywords, you can write a shim as follows:
+
+ package MyApp::DSL;
+ use Dancer2 appname => 'MyApp';
+
+ use Exporter qw< import >;
+
+ our @EXPORT = qw< get post ... >
+
+Then in other packages:
+
+ package MyApp;
+
+ use MyApp::DSL qw< get post >;
+
+ MyApp::DSL::any sub { ... };
+
+=head3 appdir
+
+This keyword does not exist in Dancer2. However, the same information can be
+found in C<< config->{'appdir'} >>.
+
+=head3 load
+
+This keyword is no longer required. Dancer2 loads the environment
+automatically and will not reload any other environment when called with
+L<load>. (It's a good thing.)
+
+=head3 param_array
+
+This keyword doesn't exist in Dancer2.
+
+=head3 session
+
+In L<Dancer> a session was created and a cookie was sent just by rendering a page
+using the C<template> function. In L<Dancer2> one needs to actually set a value in
+a session object using the C<session> function in order to create the session
+and send the cookie.
+
+The session keyword has multiple states:
+
+=over 4
+
+=item * No arguments
+
+Without any arguments, the session keyword returns a L<Dancer2::Core::Session>
+object, which has methods for L<read|Dancer2::Core::Session/read>,
+L<write|Dancer2::Core::Session/write>, and L<delete|Dancer2::Core::Session/delete>.
+
+ my $session = session;
+ $session->read($key);
+ $session->write( $key => $value );
+ $session->delete($key);
+
+=item * Single argument (key)
+
+If a single argument is provided, it is treated as the key, and it will retrieve
+the value for it.
+
+ my $value = session $key;
+
+=item * Two arguments (key, value)
+
+If two arguments are provided, they are treated as a key and a value, in which
+case the session will assign the value to the key.
+
+ session $key => $value;
+
+=item * Two arguments (key, undef)
+
+If two arguments are provided, but the second is B<undef>, the key will be
+deleted from the session.
+
+ session $key => undef;
+
+=back
+
+In Dancer 1 it wasn't possible to delete a key, but in Dancer2 we can finally
+delete:
+
+ # these two are equivalent
+ session $key => undef;
+
+ my $session = session;
+ $session->delete($key);
+
+You can retrieve the whole session hash with the C<data> method:
+
+ $session->data;
+
+To destroy a session, instead of writing:
+
+ session->destroy
+
+In Dancer2, we write:
+
+ app->destroy_session if app->has_session
+
+If you make changes to the session in an C<after> hook, your changes will
+not be written to storage, because writing sessions to storage also takes
+place in an (earlier) C<after> hook.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Manual/Testing.pod b/lib/Dancer2/Manual/Testing.pod
new file mode 100644
index 00000000..87d66aff
--- /dev/null
+++ b/lib/Dancer2/Manual/Testing.pod
@@ -0,0 +1,144 @@
+package Dancer2::Manual::Testing;
+# ABSTRACT: Writing tests for Dancer2
+
+use strict;
+use warnings;
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Manual::Testing - Writing tests for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 Basic application testing
+
+Since L<Dancer2> produces PSGI applications, you can easily write tests using
+L<Plack::Test> and provide your Dancer application as the app for testing.
+
+A basic test (which we also scaffold with L<dancer2>) looks like this:
+
+ use strict;
+ use warnings;
+
+ use Test::More tests => 4;
+ use Plack::Test;
+ use HTTP::Request::Common;
+
+ use_ok('MyApp');
+
+ # create an application
+ my $app = MyApp->to_app;
+ isa_ok( $app, 'CODE' );
+
+ # create a testing object
+ my $test = Plack::Test->create($app);
+
+ # now you can call requests on it and get responses
+ # requests are of HTTP::Request
+ # responses are of HTTP::Response
+
+ # "GET" from HTTP::Request::Common creates an HTTP::Request object
+ my $response = $test->request( GET '/' );
+
+ # same as:
+ # my $response = $test->request( HTTP::Request->new( GET => '/' ) );
+
+ ok( $response->is_success, 'Successful request' );
+ is( $response->content, 'OK', 'Correct response content' );
+
+Read the documentation for L<HTTP::Request> and L<HTTP::Request::Common> to
+see the different options for sending parameters.
+
+=head1 Cookies
+
+If you don't want to use an entire user agent for this test, you can use
+L<HTTP::Cookies> to store cookies and then retrieve them:
+
+ use strict;
+ use warnings;
+
+ use Test::More tests => 3;
+ use Plack::Test;
+ use HTTP::Request::Common;
+ use HTTP::Cookies;
+
+ use_ok('MyApp');
+
+ my $url = 'http://localhost';
+ my $jar = HTTP::Cookies->new();
+ my $test = Plack::Test->create( MyApp->to_app );
+
+ subtest 'Create session' => sub {
+ my $res = $test->request( GET "$url/login" );
+ ok( $res->is_success, 'Successful login' );
+
+ # extract cookies from the response and store in the jar
+ $jar->extract_cookies($res);
+ };
+
+ subtest 'Check session' => sub {
+ my $req = GET "$url/logout";
+
+ # add cookies to the request
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ ok( $res->is_success, 'Successful logout' );
+ like(
+ $res->content,
+ 'Successfully logged out',
+ 'Got correct log out content',
+ );
+ };
+
+Please note that the request URL must include scheme and host for the call
+to L<HTTP::Cookies/add_cookie_header> to work.
+
+=head1 Plugins
+
+In order to test plugins, you can create an application on the spot, as
+part of the test script code, and use the plugin there.
+
+ use strict;
+ use warnings;
+
+ use Test::More tests => 2;
+ use Plack::Test;
+ use HTTP::Request::Common;
+
+ {
+ package MyTestApp;
+ use Dancer2;
+ use Dancer2::Plugin::MyPlugin;
+
+ get '/' => sub { my_keyword };
+ }
+
+ my $test = Plack::Test->create( MyTestApp->to_app );
+ my $res = $test->request( GET '/' );
+
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content, 'MyPlugin-MyKeyword', 'Correct content' );
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Plugin.pm b/lib/Dancer2/Plugin.pm
new file mode 100644
index 00000000..24f245fb
--- /dev/null
+++ b/lib/Dancer2/Plugin.pm
@@ -0,0 +1,1107 @@
+package Dancer2::Plugin;
+# ABSTRACT: base class for Dancer2 plugins
+$Dancer2::Plugin::VERSION = '0.300000';
+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<is_arrayref is_coderef>;
+
+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 {};
+
+ sub plugin_args {
+ Carp::carp "Plugin DSL method 'plugin_args' is deprecated. "
+ . "Use '\\\@_' instead'.\n";
+
+ \@_;
+ }
+ }
+END
+
+ $no_dsl->{$caller} or eval <<"END"; ## no critic
+ {
+ package $caller;
+
+ # FIXME: AUTOLOAD might pick up on this
+ sub dancer_app {
+ Carp::carp "Plugin DSL method 'dancer_app' is deprecated. "
+ . "Use '\\\$self->app' instead'.\n";
+
+ \$_[0]->app;
+ }
+
+ # FIXME: AUTOLOAD might pick up on this
+ sub request {
+ Carp::carp "Plugin DSL method 'request' is deprecated. "
+ . "Use '\\\$self->app->request' instead'.\n";
+
+ \$_[0]->app->request;
+ }
+
+ # FIXME: AUTOLOAD might pick up on this
+ sub var {
+ Carp::carp "Plugin DSL method 'var' is deprecated. "
+ . "Use '\\\$self->app->request->var' instead'.\n";
+
+ shift->app->request->var(\@_);
+ }
+
+ # FIXME: AUTOLOAD might pick up on this
+ sub hook {
+ Carp::carp "Plugin DSL method 'hook' is deprecated. "
+ . "Use '\\\$self->app->add_hook' instead'.\n";
+
+ shift->app->add_hook(
+ Dancer2::Core::Hook->new( name => shift, code => shift ) );
+ }
+
+ }
+END
+
+ die $@ if $@;
+
+ my $app_dsl_cb = _find_consumer();
+
+ if ( $app_dsl_cb ) {
+ my $dsl = $app_dsl_cb->();
+
+ {
+ ## no critic qw(TestingAndDebugging::ProhibitNoWarnings)
+ no strict 'refs';
+ no warnings 'redefine';
+ *{"${caller}::dsl"} = sub {$dsl};
+ }
+ }
+
+ return map { [ $_ => { class => $caller } ] }
+ qw/ plugin_keywords plugin_hooks /;
+}
+
+sub _find_consumer {
+ my $class;
+
+ ## no critic qw(ControlStructures::ProhibitCStyleForLoops)
+ for ( my $i = 1; my $caller = caller($i); $i++ ) {
+ $class = $caller->can('dsl')
+ and last;
+ }
+
+ # If you use a Dancer2 plugin outside a Dancer App, this fails.
+ # It also breaks a bunch of the tests. -- SX
+ #$class
+ # or croak('Could not find Dancer2 app');
+
+ return $class;
+};
+
+# This has to be called for now at the end of every plugin package, in order to
+# map the keywords of the associated app to the plugin, so that these keywords
+# can be called from within the plugin code. This function is deprecated, as
+# it's tied to the old plugin system. It's kept here for backcompat reason, but
+# should go away with the old plugin system.
+sub register_plugin {
+
+ my $plugin_module = caller(1);
+
+ # if you ask yourself why we do the injection in the plugin
+ # module namespace every time the plugin is used, and not only
+ # once, it's because it can be used by different app that could
+ # have a different DSL with a different list of keywords.
+
+ my $_DANCER2_IMPORT_TIME_SUBS = $plugin_module->_DANCER2_IMPORT_TIME_SUBS;
+ unshift(@$_DANCER2_IMPORT_TIME_SUBS, sub {
+ my $app_dsl_cb = _find_consumer();
+
+ # Here we want to verify that "register_plugin" compat keyword
+ # was in fact only called from an app.
+ $app_dsl_cb
+ or Carp::croak(
+ 'I could not find a Dancer App for this plugin');
+
+ my $dsl = $app_dsl_cb->();
+
+ foreach my $keyword ( keys %{ $dsl->dsl_keywords} ) {
+ # if not yet defined, inject the keyword in the plugin
+ # namespace, but make sure the code will always get the
+ # coderef from the right associated app, because one plugin
+ # can be used by multiple apps. Note that we remove the
+ # first parameter (plugin instance) from what we pass to
+ # the keyword implementation of the App
+ no strict 'refs';
+ $plugin_module->can($keyword)
+ or *{"${plugin_module}::$keyword"} = sub {
+ my $coderef = shift()->app->name->can($keyword);
+ $coderef->(@_);
+ };
+ }
+ });
+}
+
+sub _exporter_expand_sub {
+ my( $plugin, $name, $args, $global ) = @_;
+ my $class = $args->{class};
+
+ return _exported_plugin_keywords($plugin,$class)
+ if $name eq 'plugin_keywords';
+
+ return _exported_plugin_hooks($class)
+ if $name eq 'plugin_hooks';
+
+ $exported_app->{ $global->{'into'} }
+ or Carp::croak('Specific subroutines cannot be exported from plugin');
+
+ # otherwise, we're exporting a keyword
+
+ my $p = $args->{plugin};
+ my $sub = $p->keywords->{$name};
+ return $name => sub(@) {
+ # localize the plugin so we can get it later
+ local $CUR_PLUGIN = $p;
+ $sub->($p,@_);
+ }
+}
+
+# "There's a good reason for this, I swear!"
+# -- Sawyer X
+# basically, if someone adds a hook to the app directly
+# that needs to access a DSL that needs the current object
+# (such as "plugin_setting"),
+# that object needs to be available
+# So:
+# we override App's "add_hook" to provide a register a
+# different hook callback, that closes over the plugin when
+# it's available, relocalizes it when the callback runs and
+# after localizing it, calls the original hook callback
+{
+ ## no critic;
+ no strict 'refs';
+ no warnings 'redefine';
+ my $orig_cb = Dancer2::Core::App->can('add_hook');
+ $orig_cb and *{'Dancer2::Core::App::add_hook'} = sub {
+ my ( $app, $hook ) = @_;
+
+ my $hook_code = Scalar::Util::blessed($hook) ? $hook->code : $hook->{code};
+ my $plugin = $CUR_PLUGIN;
+
+ $hook->{'code'} = sub {
+ local $CUR_PLUGIN = $plugin;
+ $hook_code->(@_);
+ };
+
+ $orig_cb->(@_);
+ };
+}
+
+
+# define the exported 'plugin_keywords'
+sub _exported_plugin_keywords{
+ my( $plugin, $class ) = @_;
+
+ return plugin_keywords => sub(@) {
+ while( my $name = shift @_ ) {
+ ## no critic
+ my $sub = is_coderef($_[0])
+ ? shift @_
+ : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name );
+ $class->keywords->{$_} = $sub for ref $name ? @$name : $name;
+ }
+ }
+}
+
+sub _exported_plugin_hooks {
+ my $class = shift;
+ return plugin_hooks => sub (@) { $class->add_hooks(@_) }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Plugin - base class for Dancer2 plugins
+
+=head1 VERSION
+
+version 0.300000
+
+=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<use Dancer2::Plugin>
+
+The plugin must begin with
+
+ use Dancer2::Plugin;
+
+which will turn the package into a L<Moo> class that inherits from L<Dancer2::Plugin>. The base class provides the plugin with
+two attributes: C<app>, which is populated with the Dancer2 app object for which
+the plugin is being initialized for, and C<config> 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<BUILD()> function.
+
+ sub BUILD {
+ my $plugin = shift;
+
+ $plugin->app->add_route( ... );
+ }
+
+=head3 Adding keywords
+
+=head4 Via C<plugin_keywords>
+
+Keywords that the plugin wishes to export to the Dancer2 app can be defined via the C<plugin_keywords> 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>.
+
+ 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<plugin_keywords> 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<config()> 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<Dancer2::Plugin> allows for a C<from_config> 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<default> 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<config> attribute is loaded lazily on the first call to
+C<config>. After this first call C<config> 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<app()>.
+
+ 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<Dancer2::Manual/"DSL KEYWORDS"> 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<app()> 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 C<use>ing 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<smiley()> 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<find_plugin> 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>.
+
+ plugin_hooks 'my_hook', 'my_other_hook';
+
+Hooks are prefixed with C<plugin.plugin_name>. So the plugin
+C<my_hook> coming from the plugin C<Dancer2::Plugin::MyPlugin> will have the hook name
+C<plugin.myplugin.my_hook>.
+
+Hooks are executed within the plugin by calling them via the associated I<app>.
+
+ $plugin->execute_plugin_hook( 'my_hook' );
+
+You can also call any other hook if you provide the full name using the
+C<execute_hook> method:
+
+ $plugin->app->execute_hook( 'core.app.route_exception' );
+
+Or using their alias:
+
+ $plugin->app->execute_hook( 'on_route_exception' );
+
+B<Note:> 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<BEGIN> 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<plugin_keywords> after the attribute definition.
+
+ package Dancer2::Plugin::Foo;
+
+ use Dancer2::Plugin;
+
+ has bar => (
+ is => 'ro',
+ );
+
+ plugin_keywords 'bar';
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Plugins.pod b/lib/Dancer2/Plugins.pod
new file mode 100644
index 00000000..e1068b93
--- /dev/null
+++ b/lib/Dancer2/Plugins.pod
@@ -0,0 +1,136 @@
+package Dancer2::Plugins;
+# ABSTRACT: Recommended Dancer2 plugins
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Plugins - Recommended Dancer2 plugins
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+Dancer2 aims to keep the core as small as possible, but there are a growing
+number of useful plugins to add helpful features.
+
+This document provides a quick summary of some recommended plugins.
+
+=head2 Plugins
+
+=over 4
+
+=item L<Dancer2::Plugin::Adapter>
+
+Wrap any simple class as a service for Dancer2.
+
+=item L<Dancer2::Plugin::Ajax>
+
+Provides easy way to add Ajax route handlers.
+
+=item L<Dancer2::Plugin::Auth::Tiny>
+
+Provides an extremely simple way of requiring that a user be logged in
+before allowing access to certain routes.
+
+=item L<Dancer2::Plugin::BrowserDetect>
+
+Provides an easy to have info of the browser. keyword within your
+Dancer application.
+
+=item L<Dancer2::Plugin::Cache::CHI>
+
+Provides caching for generated pages and/or arbitrary data. Uses L<CHI>, so
+is backend-agnostic - caching can be done in memory, to files, using
+Memcache, in a database, or any other method for which there is a
+L<CHI::Driver> module.
+
+=item L<Dancer2::Plugin::Database>
+
+Provides easy database access via DBI, reading the DB connection details
+from your app's config file, and taking care of ensuring the connection is
+still valid and reconnecting if not (useful in persistent environments).
+Just calling the C<database> keyword gives you a connected and working
+database handle. It also provides some helpful keywords to make
+inserting/updating data as simple as it should be.
+
+=item L<Dancer2::Plugin::DBIC>
+
+Provides easy access to DBIx::Class database virtualization.
+
+=item L<Dancer2::Plugin::Deferred>
+
+Provides a method for deferring a one-time message across a redirect. It is
+similar to "flash" messages, but without the race conditions that can result
+from multiple tabs in a browser or from AJAX requests.
+
+=item L<Dancer2::Plugin::Emailesque>
+
+Provides easy email-sending powered by Email::Send - simply call the
+C<email> keyword. Email sending settings can be taken from your app's
+config.
+
+=item L<Dancer2::Plugin::Feed>
+
+Provides an easy way to generate RSS or Atom feed.
+
+=item L<Dancer2::Plugin::Paginator>
+
+Dancer2 plugin for Paginator::Lite.
+
+=item L<Dancer2::Plugin::Queue>
+
+Provides a generic interface to a message queue.
+
+=item L<Dancer2::Plugin::Queue::MongoDB>
+
+A L<Dancer2::Plugin::Queue> using L<MongoDBx::Queue>.
+
+=item L<Dancer2::Plugin::REST>
+
+Makes writing RESTful web services easy.
+
+=item L<Dancer2::Plugin::RoutePodCoverage>
+
+Plugin to verify pod coverage in our app routes.
+
+=item L<Dancer2::Plugin::Syntax::GetPost>
+
+Provides very simple syntactic sugar to define a handler for GET and POST
+requests.
+
+=back
+
+=head2 Session Engines
+
+=over 4
+
+=item L<Dancer2::Session::Cookie>
+
+A session factory for Dancer2 that stores session state within a browser
+cookie.
+
+=back
+
+More plugins are appearing on CPAN all the time - just search for
+C<Dancer2::Plugin> to see what may have been released since this document
+was last updated!
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Policy.pod b/lib/Dancer2/Policy.pod
new file mode 100644
index 00000000..0c786c3d
--- /dev/null
+++ b/lib/Dancer2/Policy.pod
@@ -0,0 +1,121 @@
+package 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 0.300000
+
+=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),
+Steven Humphrey, 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<your> 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<p5p> (the Perl 5 Porters).
+
+The original inspiration policy document can be read at L<perlpolicy>.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Serializer/Dumper.pm b/lib/Dancer2/Serializer/Dumper.pm
new file mode 100644
index 00000000..87e1c968
--- /dev/null
+++ b/lib/Dancer2/Serializer/Dumper.pm
@@ -0,0 +1,101 @@
+# ABSTRACT: Serializer for handling Dumper data
+
+package Dancer2::Serializer::Dumper;
+$Dancer2::Serializer::Dumper::VERSION = '0.300000';
+use Moo;
+use Carp 'croak';
+use Data::Dumper;
+use Safe;
+
+with 'Dancer2::Core::Role::Serializer';
+
+has '+content_type' => ( default => sub {'text/x-data-dumper'} );
+
+# helpers
+sub from_dumper { __PACKAGE__->deserialize(@_) }
+
+sub to_dumper { __PACKAGE__->serialize(@_) }
+
+# class definition
+sub serialize {
+ my ( $self, $entity ) = @_;
+
+ {
+ local $Data::Dumper::Purity = 1;
+ return Dumper($entity);
+ }
+}
+
+sub deserialize {
+ my ( $self, $content ) = @_;
+
+ my $cpt = Safe->new;
+
+ my $res = $cpt->reval("my \$VAR1; $content");
+ croak "unable to deserialize : $@" if $@;
+ return $res;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Serializer::Dumper - Serializer for handling Dumper data
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This is a serializer engine that allows you to turn Perl data structures into
+L<Data::Dumper> output and vice-versa.
+
+=head1 ATTRIBUTES
+
+=head2 content_type
+
+Returns 'text/x-data-dumper'
+
+=head1 METHODS
+
+=head2 serialize($content)
+
+Serializes a Perl data structure into a Dumper string.
+
+=head2 deserialize($content)
+
+Deserialize a Dumper string into a Perl data structure.
+
+=head1 FUNCTIONS
+
+=head2 from_dumper($content)
+
+This is an helper available to transform a L<Data::Dumper> output to a Perl
+data structures.
+
+=head2 to_dumper($content)
+
+This is an helper available to transform a Perl data structures to a
+L<Data::Dumper> output.
+
+Calling this function will B<not> trigger the serialization's hooks.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Serializer/JSON.pm b/lib/Dancer2/Serializer/JSON.pm
new file mode 100644
index 00000000..66e059ee
--- /dev/null
+++ b/lib/Dancer2/Serializer/JSON.pm
@@ -0,0 +1,153 @@
+package Dancer2::Serializer::JSON;
+# ABSTRACT: Serializer for handling JSON data
+$Dancer2::Serializer::JSON::VERSION = '0.300000';
+use Moo;
+use JSON::MaybeXS ();
+use Scalar::Util 'blessed';
+
+with 'Dancer2::Core::Role::Serializer';
+
+has '+content_type' => ( default => sub {'application/json'} );
+
+# helpers
+sub from_json { __PACKAGE__->deserialize(@_) }
+
+sub to_json { __PACKAGE__->serialize(@_) }
+
+sub decode_json {
+ my ( $entity ) = @_;
+
+ JSON::MaybeXS::decode_json($entity);
+}
+
+sub encode_json {
+ my ( $entity ) = @_;
+
+ JSON::MaybeXS::encode_json($entity);
+}
+
+# class definition
+sub serialize {
+ my ( $self, $entity, $options ) = @_;
+
+ my $config = blessed $self ? $self->config : {};
+
+ foreach (keys %$config) {
+ $options->{$_} = $config->{$_} unless exists $options->{$_};
+ }
+
+ $options->{utf8} = 1 if !defined $options->{utf8};
+ JSON::MaybeXS->new($options)->encode($entity);
+}
+
+sub deserialize {
+ my ( $self, $entity, $options ) = @_;
+
+ $options->{utf8} = 1 if !defined $options->{utf8};
+ JSON::MaybeXS->new($options)->decode($entity);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Serializer::JSON - Serializer for handling JSON data
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This is a serializer engine that allows you to turn Perl data structures into
+JSON output and vice-versa.
+
+=head1 ATTRIBUTES
+
+=head2 content_type
+
+Returns 'application/json'
+
+=head1 METHODS
+
+=head2 serialize($content)
+
+Serializes a Perl data structure into a JSON string.
+
+=head2 deserialize($content)
+
+Deserializes a JSON string into a Perl data structure.
+
+=head1 FUNCTIONS
+
+=head2 from_json($content, \%options)
+
+This is an helper available to transform a JSON data structure to a Perl data structures.
+
+=head2 to_json($content, \%options)
+
+This is an helper available to transform a Perl data structure to JSON.
+
+Calling this function will B<not> trigger the serialization's hooks.
+
+=head2 Configuring the JSON Serializer using C<set engines>
+
+The JSON serializer options can be configured via C<set engines>. The most
+common settings are:
+
+=over 4
+
+=item allow_nonref
+
+Ignore non-ref scalars returned from handlers. With this set the "Hello, World!"
+handler returning a string will be dealt with properly.
+
+=back
+
+Set engines should be called prior to setting JSON as the serializer:
+
+ set engines =>
+ {
+ serializer =>
+ {
+ JSON =>
+ {
+ allow_nonref => 1
+ },
+ }
+ };
+
+ set serializer => 'JSON';
+ set content_type => 'application/json';
+
+=head2 Returning non-JSON data.
+
+Handlers can return non-JSON via C<send_as>, which overrides the default serializer:
+
+ get '/' =>
+ sub
+ {
+ send_as html =>
+ q{Welcome to the root of all evil...<br>step into my office.}
+ };
+
+Any other non-JSON returned format supported by 'send_as' can be used.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Serializer/Mutable.pm b/lib/Dancer2/Serializer/Mutable.pm
new file mode 100644
index 00000000..fc36be5f
--- /dev/null
+++ b/lib/Dancer2/Serializer/Mutable.pm
@@ -0,0 +1,246 @@
+package Dancer2::Serializer::Mutable;
+# ABSTRACT: Serialize and deserialize content based on HTTP header
+$Dancer2::Serializer::Mutable::VERSION = '0.300000';
+use Moo;
+use Carp 'croak';
+use Encode;
+with 'Dancer2::Core::Role::Serializer';
+
+use constant DEFAULT_CONTENT_TYPE => 'application/json';
+
+has '+content_type' => ( default => DEFAULT_CONTENT_TYPE() );
+
+my $serializer = {
+ 'YAML' => {
+ to => sub { Dancer2::Core::DSL::to_yaml(@_) },
+ from => sub { Dancer2::Core::DSL::from_yaml(@_) },
+ },
+ 'Dumper' => {
+ to => sub { Dancer2::Core::DSL::to_dumper(@_) },
+ from => sub { Dancer2::Core::DSL::from_dumper(@_) },
+ },
+ 'JSON' => {
+ to => sub { Dancer2::Core::DSL::to_json(@_) },
+ from => sub { Dancer2::Core::DSL::from_json(@_) },
+ },
+};
+
+has mapping => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+
+ if ( my $mapping = $self->config->{mapping} ) {
+
+ # initialize non-default serializers
+ for my $s ( values %$mapping ) {
+ # TODO allow for arguments via the config
+ next if $serializer->{$s};
+ my $serializer_object = ('Dancer2::Serializer::'.$s)->new;
+ $serializer->{$s} = {
+ from => sub { shift; $serializer_object->deserialize(@_) },
+ to => sub { shift; $serializer_object->serialize(@_) },
+ };
+ }
+
+ return $mapping;
+ }
+
+
+ return {
+ 'text/x-yaml' => 'YAML',
+ 'text/html' => 'YAML',
+ 'text/x-data-dumper' => 'Dumper',
+ 'text/x-json' => 'JSON',
+ 'application/json' => 'JSON',
+ }
+ },
+);
+
+sub serialize {
+ my ( $self, $entity ) = @_;
+
+ # Look for valid format in the headers
+ my $format = $self->_get_content_type('accept');
+
+ # Match format with a serializer and return
+ $format and return $serializer->{$format}{'to'}->(
+ $self, $entity
+ );
+
+ # If none is found then just return the entity without change
+ return $entity;
+}
+
+sub deserialize {
+ my ( $self, $content ) = @_;
+
+ my $format = $self->_get_content_type('content_type');
+ $format and return $serializer->{$format}{'from'}->($self, $content);
+
+ return $content;
+}
+
+sub _get_content_type {
+ my ($self, $header) = @_;
+
+ if ( $self->has_request ) {
+ # Search for the first HTTP header variable which specifies
+ # supported content. Both content_type and accept are checked
+ # for backwards compatibility.
+ foreach my $method ( $header, qw<content_type accept> ) {
+ if ( my $value = $self->request->header($method) ) {
+ if ( my $serializer = $self->mapping->{$value} ) {
+ $self->set_content_type($value);
+ return $serializer;
+ }
+ }
+ }
+ }
+
+ # If none if found, return the default, 'JSON'.
+ $self->set_content_type( DEFAULT_CONTENT_TYPE() );
+ return 'JSON';
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Serializer::Mutable - Serialize and deserialize content based on HTTP header
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ # in config.yml
+ serializer: Mutable
+
+ engines:
+ serializer:
+ Mutable:
+ mapping:
+ 'text/x-yaml' : YAML
+ 'text/html' : YAML
+ 'text/x-data-dumper' : Dumper
+ 'text/x-json' : JSON
+ 'application/json' : JSON
+
+ # in the app
+ put '/something' => sub {
+ # deserialized from request
+ my $name = param( 'name' );
+
+ ...
+
+ # will be serialized to the most
+ # fitting format
+ return { message => "user $name added" };
+ };
+
+=head1 DESCRIPTION
+
+This serializer will try find the best (de)serializer for a given request.
+For this, it will pick the first valid content type found from the following list
+and use its related serializer.
+
+=over
+
+=item
+
+The B<content_type> from the request headers
+
+=item
+
+the B<accept> from the request headers
+
+=item
+
+The default is B<application/json>
+
+=back
+
+The content-type/serializer mapping that C<Dancer2::Serializer::Mutable>
+uses is
+
+ serializer | content types
+ ----------------------------------------------------------
+ Dancer2::Serializer::YAML | text/x-yaml, text/html
+ Dancer2::Serializer::Dumper | text/x-data-dumper
+ Dancer2::Serializer::JSON | text/x-json, application/json
+
+A different mapping can be provided via the config file. For example,
+the default mapping would be configured as
+
+ engines:
+ serializer:
+ Mutable:
+ mapping:
+ 'text/x-yaml' : YAML
+ 'text/html' : YAML
+ 'text/x-data-dumper' : Dumper
+ 'text/x-json' : JSON
+ 'application/json' : JSON
+
+The keys of the mapping are the content-types to serialize,
+and the values the serializers to use. Serialization for C<YAML>, C<Dumper>
+and C<JSON> are done using internal Dancer mechanisms. Any other serializer will
+be taken to be as Dancer2 serialization class (minus the C<Dancer2::Serializer::> prefix)
+and an instance of it will be used
+to serialize/deserialize data. For example, adding L<Dancer2::Serializer::XML>
+to the mapping would be:
+
+ engines:
+ serializer:
+ Mutable:
+ mapping:
+ 'text/x-yaml' : YAML
+ 'text/html' : YAML
+ 'text/x-data-dumper' : Dumper
+ 'text/x-json' : JSON
+ 'text/xml' : XML
+
+=head2 INTERNAL METHODS
+
+The following methods are used internally by C<Dancer2> and are not made
+accessible via the DSL.
+
+=head2 serialize
+
+Serialize a data structure. The format it is serialized to is determined
+automatically as described above. It can be one of YAML, Dumper, JSON, defaulting
+to JSON if there's no clear preference from the request.
+
+=head2 deserialize
+
+Deserialize the provided serialized data to a data structure. The type of
+serialization format depends on the request's content-type. For now, it can
+be one of YAML, Dumper, JSON.
+
+=head2 content_type
+
+Returns the content-type that was used during the last C<serialize> /
+C<deserialize> call. B<WARNING> : you must call C<serialize> / C<deserialize>
+before calling C<content_type>. Otherwise the return value will be C<undef>.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Serializer/YAML.pm b/lib/Dancer2/Serializer/YAML.pm
new file mode 100644
index 00000000..8fe50a3f
--- /dev/null
+++ b/lib/Dancer2/Serializer/YAML.pm
@@ -0,0 +1,91 @@
+package Dancer2::Serializer::YAML;
+# ABSTRACT: Serializer for handling YAML data
+$Dancer2::Serializer::YAML::VERSION = '0.300000';
+use Moo;
+use Carp 'croak';
+use Encode;
+use Module::Runtime 'use_module';
+
+with 'Dancer2::Core::Role::Serializer';
+
+has '+content_type' => ( default => sub {'text/x-yaml'} );
+
+# helpers
+sub from_yaml { __PACKAGE__->deserialize(@_) }
+
+sub to_yaml { __PACKAGE__->serialize(@_) }
+
+# class definition
+sub BUILD { use_module('YAML') }
+
+sub serialize {
+ my ( $self, $entity ) = @_;
+ encode('UTF-8', YAML::Dump($entity));
+}
+
+sub deserialize {
+ my ( $self, $content ) = @_;
+ YAML::Load(decode('UTF-8', $content));
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Serializer::YAML - Serializer for handling YAML data
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This is a serializer engine that allows you to turn Perl data structures into
+YAML output and vice-versa.
+
+=head1 ATTRIBUTES
+
+=head2 content_type
+
+Returns 'text/x-yaml'
+
+=head1 METHODS
+
+=head2 serialize($content)
+
+Serializes a data structure to a YAML structure.
+
+=head2 deserialize($content)
+
+Deserializes a YAML structure to a data structure.
+
+=head1 FUNCTIONS
+
+=head2 fom_yaml($content)
+
+This is an helper available to transform a YAML data structure to a Perl data structures.
+
+=head2 to_yaml($content)
+
+This is an helper available to transform a Perl data structure to YAML.
+
+Calling this function will B<not> trigger the serialization's hooks.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Session/Simple.pm b/lib/Dancer2/Session/Simple.pm
new file mode 100644
index 00000000..616c7840
--- /dev/null
+++ b/lib/Dancer2/Session/Simple.pm
@@ -0,0 +1,93 @@
+package Dancer2::Session::Simple;
+# ABSTRACT: in-memory session backend for Dancer2
+$Dancer2::Session::Simple::VERSION = '0.300000';
+use Moo;
+use Dancer2::Core::Types;
+use Carp;
+
+with 'Dancer2::Core::Role::SessionFactory';
+
+# The singleton that contains all the session objects created
+my $SESSIONS = {};
+
+sub _sessions {
+ my ($self) = @_;
+ return [ keys %{$SESSIONS} ];
+}
+
+sub _retrieve {
+ my ( $class, $id ) = @_;
+ my $s = $SESSIONS->{$id};
+
+ croak "Invalid session ID: $id"
+ if !defined $s;
+
+ return $s;
+}
+
+sub _change_id {
+ my ( $class, $old_id, $new_id ) = @_;
+
+ $SESSIONS->{$new_id} = $class->_retrieve($old_id);
+ delete $SESSIONS->{$old_id};
+}
+
+sub _destroy {
+ my ( $class, $id ) = @_;
+ delete $SESSIONS->{$id};
+}
+
+sub _flush {
+ my ( $class, $id, $data ) = @_;
+ $SESSIONS->{$id} = $data;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Session::Simple - in-memory session backend for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This module implements a very simple session backend, holding all session data
+in memory. This means that sessions are volatile, and no longer exist when the
+process exits. This module is likely to be most useful for testing purposes.
+
+=head1 DISCLAIMER
+
+This session factory should not be used in production and is only for
+single-process application workers. As the sessions objects are stored
+in-memory, they cannot be shared among multiple workers.
+
+=head1 CONFIGURATION
+
+The setting B<session> should be set to C<Simple> in order to use this session
+engine in a Dancer2 application.
+
+=head1 SEE ALSO
+
+See L<Dancer2::Core::Session> for details about session usage in route handlers.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Session/YAML.pm b/lib/Dancer2/Session/YAML.pm
new file mode 100644
index 00000000..fe0ec8fc
--- /dev/null
+++ b/lib/Dancer2/Session/YAML.pm
@@ -0,0 +1,95 @@
+package Dancer2::Session::YAML;
+$Dancer2::Session::YAML::VERSION = '0.300000';
+# ABSTRACT: YAML-file-based session backend for Dancer2
+
+use Moo;
+use Dancer2::Core::Types;
+use YAML;
+
+has _suffix => (
+ is => 'ro',
+ isa => Str,
+ default => sub {'.yml'},
+);
+
+with 'Dancer2::Core::Role::SessionFactory::File';
+
+sub _freeze_to_handle {
+ my ( $self, $fh, $data ) = @_;
+ print {$fh} YAML::Dump($data);
+ return;
+}
+
+sub _thaw_from_handle {
+ my ( $self, $fh ) = @_;
+ return YAML::LoadFile($fh);
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Session::YAML - YAML-file-based session backend for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 DESCRIPTION
+
+This module implements a session engine based on YAML files. Session are stored
+in a I<session_dir> as YAML files. The idea behind this module was to provide a
+human-readable session storage for the developer.
+
+This backend is intended to be used in development environments, when digging
+inside a session can be useful.
+
+This backend can perfectly be used in production environments, but two things
+should be kept in mind: The content of the session files is in plain text, and
+the session files should be purged by a CRON job.
+
+=head1 CONFIGURATION
+
+The setting B<session> should be set to C<YAML> in order to use this session
+engine in a Dancer2 application.
+
+Files will be stored to the value of the setting C<session_dir>, whose default
+value is C<appdir/sessions>.
+
+Here is an example configuration that use this session engine and stores session
+files in /tmp/dancer-sessions
+
+ session: "YAML"
+
+ engines:
+ session:
+ YAML:
+ session_dir: "/tmp/dancer-sessions"
+ cookie_duration: 3600 # Default cookie timeout in seconds
+
+=head1 DEPENDENCY
+
+This module depends on L<YAML>.
+
+=head1 SEE ALSO
+
+See L<Dancer2::Core::Session> for details about session usage in route handlers.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Template/Implementation/ForkedTiny.pm b/lib/Dancer2/Template/Implementation/ForkedTiny.pm
new file mode 100644
index 00000000..07bcb6e5
--- /dev/null
+++ b/lib/Dancer2/Template/Implementation/ForkedTiny.pm
@@ -0,0 +1,390 @@
+package Dancer2::Template::Implementation::ForkedTiny;
+$Dancer2::Template::Implementation::ForkedTiny::VERSION = '0.300000';
+# ABSTRACT: Dancer2 own implementation of Template::Tiny
+
+use 5.00503;
+use strict;
+no warnings;
+use Ref::Util qw<is_arrayref is_coderef is_plain_hashref>;
+
+# Evaluatable expression
+my $EXPR = qr/ [a-z_][\w.]* /xs;
+
+sub new {
+ my $self = bless {
+ start_tag => '[%',
+ end_tag => '%]',
+ @_[ 1 .. $#_ ]
+ },
+ $_[0];
+
+# Opening tag including whitespace chomping rules
+ my $LEFT = $self->{LEFT} = qr/
+ (?:
+ (?: (?:^|\n) [ \t]* )? \Q$self->{start_tag}\E\-
+ |
+ \Q$self->{start_tag}\E \+?
+ ) \s*
+/xs;
+
+# Closing %] tag including whitespace chomping rules
+ my $RIGHT = $self->{RIGHT} = qr/
+ \s* (?:
+ \+? \Q$self->{end_tag}\E
+ |
+ \-\Q$self->{end_tag}\E (?: [ \t]* \n )?
+ )
+/xs;
+
+# Preparsing run for nesting tags
+ $self->{PREPARSE} = qr/
+ $LEFT ( IF | UNLESS | FOREACH ) \s+
+ (
+ (?: \S+ \s+ IN \s+ )?
+ \S+ )
+ $RIGHT
+ (?!
+ .*?
+ $LEFT (?: IF | UNLESS | FOREACH ) \b
+ )
+ ( .*? )
+ (?:
+ $LEFT ELSE $RIGHT
+ (?!
+ .*?
+ $LEFT (?: IF | UNLESS | FOREACH ) \b
+ )
+ ( .+? )
+ )?
+ $LEFT END $RIGHT
+/xs;
+
+ $self->{CONDITION} = qr/
+ \Q$self->{start_tag}\E\s
+ ( ([IUF])\d+ ) \s+
+ (?:
+ ([a-z]\w*) \s+ IN \s+
+ )?
+ ( $EXPR )
+ \s\Q$self->{end_tag}\E
+ ( .*? )
+ (?:
+ \Q$self->{start_tag}\E\s \1 \s\Q$self->{end_tag}\E
+ ( .+? )
+ )?
+ \Q$self->{start_tag}\E\s \1 \s\Q$self->{end_tag}\E
+/xs;
+
+ $self;
+}
+
+# Copy and modify
+sub preprocess {
+ my $self = shift;
+ my $text = shift;
+ $self->_preprocess( \$text );
+ return $text;
+}
+
+sub process {
+ my $self = shift;
+ my $copy = ${ shift() };
+ my $stash = shift || {};
+
+ local $@ = '';
+ local $^W = 0;
+
+ # Preprocess to establish unique matching tag sets
+ $self->_preprocess( \$copy );
+
+ # Process down the nested tree of conditions
+ my $result = $self->_process( $stash, $copy );
+ if (@_) {
+ ${ $_[0] } = $result;
+ }
+ elsif ( defined wantarray ) {
+ require Carp;
+ Carp::carp(
+ 'Returning of template results is deprecated in Template::Tiny 0.11'
+ );
+ return $result;
+ }
+ else {
+ print $result;
+ }
+}
+
+
+######################################################################
+# Support Methods
+
+# The only reason this is a standalone is so we can
+# do more in-depth testing.
+sub _preprocess {
+ my $self = shift;
+ my $copy = shift;
+
+ # Preprocess to establish unique matching tag sets
+ my $id = 0;
+ 1 while $$copy =~ s/
+ $self->{PREPARSE}
+ /
+ my $tag = substr($1, 0, 1) . ++$id;
+ "\[\% $tag $2 \%\]$3\[\% $tag \%\]"
+ . (defined($4) ? "$4\[\% $tag \%\]" : '');
+ /sex;
+}
+
+sub _process {
+ my ( $self, $stash, $text ) = @_;
+
+ $text =~ s/
+ $self->{CONDITION}
+ /
+ ($2 eq 'F')
+ ? $self->_foreach($stash, $3, $4, $5)
+ : eval {
+ $2 eq 'U'
+ xor
+ !! # Force boolification
+ $self->_expression($stash, $4)
+ }
+ ? $self->_process($stash, $5)
+ : $self->_process($stash, $6)
+ /gsex;
+
+ # Resolve expressions
+ $text =~ s/
+ $self->{LEFT} ( $EXPR ) $self->{RIGHT}
+ /
+ eval {
+ $self->_expression($stash, $1)
+ . '' # Force stringification
+ }
+ /gsex;
+
+ # Trim the document
+ $text =~ s/^\s*(.+?)\s*\z/$1/s if $self->{TRIM};
+
+ return $text;
+}
+
+# Special handling for foreach
+sub _foreach {
+ my ( $self, $stash, $term, $expr, $text ) = @_;
+
+ # Resolve the expression
+ my $list = $self->_expression( $stash, $expr );
+ is_arrayref($list) or return '';
+
+ # Iterate
+ return join '',
+ map { $self->_process( { %$stash, $term => $_ }, $text ) } @$list;
+}
+
+# Evaluates a stash expression
+sub _expression {
+ my $cursor = $_[1];
+ my @path = split /\./, $_[2];
+ foreach (@path) {
+
+ # Support for private keys
+ return if substr( $_, 0, 1 ) eq '_';
+
+ # Split by data type
+ ref $cursor or return '';
+ if ( is_arrayref($cursor) ) {
+ return '' unless /^(?:0|[0-9]\d*)\z/;
+ $cursor = $cursor->[$_];
+ }
+ elsif ( is_plain_hashref($cursor) ) {
+ $cursor = $cursor->{$_};
+ }
+ else {
+ $cursor = $cursor->$_();
+ }
+ }
+
+ # If the last expression is a CodeRef, execute it
+ is_coderef($cursor)
+ and $cursor = $cursor->();
+ return $cursor;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Template::Implementation::ForkedTiny - Dancer2 own implementation of Template::Tiny
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+ my $template = Dancer2::Template::Implementation::ForkedTiny->new(
+ TRIM => 1,
+ );
+
+ # Print the template results to STDOUT
+ $template->process( <<'END_TEMPLATE', { foo => 'World' } );
+ Hello [% foo %]!
+ END_TEMPLATE
+
+=head1 DESCRIPTION
+
+B<Dancer2::Template::Implementation::ForkedTiny> is a reimplementation of a subset of the functionality from
+L<Template> Toolkit in as few lines of code as possible.
+
+It is intended for use in light-usage, low-memory, or low-cpu templating
+situations, where you may need to upgrade to the full feature set in the
+future, or if you want the retain the familiarity of TT-style templates.
+
+For the subset of functionality it implements, it has fully-compatible template
+and stash API. All templates used with B<Dancer2::Template::Implementation::ForkedTiny> should be able to be
+transparently upgraded to full Template Toolkit.
+
+Unlike Template Toolkit, B<Dancer2::Template::Implementation::ForkedTiny> will process templates without a
+compile phase (but despite this is still quicker, owing to heavy use of
+the Perl regular expression engine.
+
+=head2 SUPPORTED USAGE
+
+By default, the C<[% %]> tag style is used. You can change the start tag and
+end tag by specifying them at object creation :
+
+ my $template = Dancer2::Template::Implementation::ForkedTiny->new(
+ start_tag => '<%',
+ end_tag => '%>,
+ );
+
+In the rest of the documentation, C<[% %]> will be used, but it can be of
+course your specified start / end tags.
+
+Both the C<[%+ +%]> style explicit whitespace and the C<[%- -%]> style
+explicit chomp B<are> support, although the C<[%+ +%]> version is unneeded
+in practice as B<Dancer2::Template::Implementation::ForkedTiny> does not support default-enabled C<PRE_CHOMP>
+or C<POST_CHOMP>.
+
+Variable expressions in the form C<[% foo.bar.baz %]> B<are> supported.
+
+Appropriate simple behaviours for C<ARRAY> references, C<HASH> references and
+objects are supported. "VMethods" such as [% array.length %] are B<not>
+supported at this time.
+
+If the resulting expression is a CodeRef, it'll be evaluated.
+
+C<IF>, C<ELSE> and C<UNLESS> conditional blocks B<are> supported, but only with
+simple C<[% foo.bar.baz %]> conditions.
+
+Support for looping (or rather iteration) is available in simple
+C<[% FOREACH item IN list %]> form B<is> supported. Other loop structures are
+B<not> supported. Because support for arbitrary or infinite looping is not
+available, B<Dancer2::Template::Implementation::ForkedTiny> templates are not turing complete. This is
+intentional.
+
+All of the four supported control structures C<IF>/C<ELSE>/C<UNLESS>/C<FOREACH>
+can be nested to arbitrary depth.
+
+The treatment of C<_private> hash and method keys is compatible with
+L<Template> Toolkit, returning null or false rather than the actual content
+of the hash key or method.
+
+Anything beyond the above is currently out of scope.
+
+=head1 NAME
+
+Dancer2::Template::Implementation::ForkedTiny - Template Toolkit reimplemented in as little code as possible, forked from Template::Tiny
+
+=head1 METHODS
+
+=head2 new
+
+ my $template = Dancer2::Template::Implementation::ForkedTiny->new(
+ TRIM => 1,
+ );
+
+The C<new> constructor is provided for compatibility with Template Toolkit.
+
+The only parameter it currently supports is C<TRIM> (which removes leading
+and trailing whitespace from processed templates).
+
+Additional parameters can be provided without error, but will be ignored.
+
+=head2 process
+
+ # DEPRECATED: Return template results (emits a warning)
+ my $text = $template->process( \$input, $vars );
+
+ # Print template results to STDOUT
+ $template->process( \$input, $vars );
+
+ # Generate template results into a variable
+ my $output = '';
+ $template->process( \$input, $vars, \$output );
+
+The C<process> method is called to process a template.
+
+The first parameter is a reference to a text string containing the template
+text. A reference to a hash may be passed as the second parameter containing
+definitions of template variables.
+
+If a third parameter is provided, it must be a scalar reference to be
+populated with the output of the template.
+
+For a limited amount of time, the old deprecated interface will continue to
+be supported. If C<process> is called without a third parameter, and in
+scalar or list contest, the template results will be returned to the caller.
+
+If C<process> is called without a third parameter, and in void context, the
+template results will be C<print()>ed to the currently selected file handle
+(probably C<STDOUT>) for compatibility with L<Template>.
+
+=head1 SUPPORT
+
+Bugs should be reported via the CPAN bug tracker at
+
+L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Template-Tiny>
+
+For other issues, or commercial enhancement or support, contact the author.
+
+=head1 AUTHOR
+
+Adam Kennedy E<lt>adamk@cpan.orgE<gt>
+
+Forked and improved by Damien Krotkine E<lt>dams@cpan.orgE<gt>
+
+=head1 SEE ALSO
+
+L<Config::Tiny>, L<CSS::Tiny>, L<YAML::Tiny>
+
+=head1 COPYRIGHT
+
+Copyright 2009 - 2011 Adam Kennedy.
+Copyright 2012 Damien Krotkine.
+
+This program is free software; you can redistribute
+it and/or modify it under the same terms as Perl itself.
+
+The full text of the license can be found in the
+LICENSE file included with this module.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Template/Simple.pm b/lib/Dancer2/Template/Simple.pm
new file mode 100644
index 00000000..817f9d72
--- /dev/null
+++ b/lib/Dancer2/Template/Simple.pm
@@ -0,0 +1,220 @@
+package Dancer2::Template::Simple;
+# ABSTRACT: Pure Perl 5 template engine for Dancer2
+$Dancer2::Template::Simple::VERSION = '0.300000';
+use Moo;
+use Dancer2::FileUtils 'read_file_content';
+use Ref::Util qw<is_arrayref is_coderef is_plain_hashref>;
+
+with 'Dancer2::Core::Role::Template';
+
+has start_tag => (
+ is => 'rw',
+ default => sub {'<%'},
+);
+
+has stop_tag => (
+ is => 'rw',
+ default => sub {'%>'},
+);
+
+sub BUILD {
+ my $self = shift;
+ my $settings = $self->config;
+
+ $settings->{$_} and $self->$_( $settings->{$_} )
+ for qw/ start_tag stop_tag /;
+}
+
+sub render {
+ my ( $self, $template, $tokens ) = @_;
+ my $content;
+
+ $content = read_file_content($template);
+ $content = $self->parse_branches( $content, $tokens );
+
+ return $content;
+}
+
+sub parse_branches {
+ my ( $self, $content, $tokens ) = @_;
+ my ( $start, $stop ) = ( $self->start_tag, $self->stop_tag );
+
+ my @buffer;
+ my $prefix = "";
+ my $should_bufferize = 1;
+ my $bufferize_if_token = 0;
+
+# $content =~ s/\Q${start}\E(\S)/${start} $1/sg;
+# $content =~ s/(\S)\Q${stop}\E/$1 ${stop}/sg;
+
+ # we get here a list of tokens without the start/stop tags
+ my @full = split( /\Q$start\E\s*(.*?)\s*\Q$stop\E/, $content );
+
+ # and here a list of tokens without variables
+ my @flat = split( /\Q$start\E\s*.*?\s*\Q$stop\E/, $content );
+
+ # eg: for 'foo=<% var %>'
+ # @full = ('foo=', 'var')
+ # @flat = ('foo=')
+
+ my $flat_index = 0;
+ my $full_index = 0;
+ for my $word (@full) {
+
+ # flat word, nothing to do
+ if ( defined $flat[$flat_index]
+ && ( $flat[$flat_index] eq $full[$full_index] ) )
+ {
+ push @buffer, $word if $should_bufferize;
+ $flat_index++;
+ $full_index++;
+ next;
+ }
+
+ my @to_parse = ($word);
+ @to_parse = split( /\s+/, $word ) if $word =~ /\s+/;
+
+ for my $w (@to_parse) {
+
+ if ( $w eq 'if' ) {
+ $bufferize_if_token = 1;
+ }
+ elsif ( $w eq 'else' ) {
+ $should_bufferize = !$should_bufferize;
+ }
+ elsif ( $w eq 'end' ) {
+ $should_bufferize = 1;
+ }
+ elsif ($bufferize_if_token) {
+ my $bool = _find_value_from_token_name( $w, $tokens );
+ $should_bufferize = _interpolate_value($bool) ? 1 : 0;
+ $bufferize_if_token = 0;
+ }
+ elsif ($should_bufferize) {
+ my $val =
+ _interpolate_value(
+ _find_value_from_token_name( $w, $tokens ) );
+ push @buffer, $val;
+ }
+ }
+
+ $full_index++;
+ }
+
+ return join "", @buffer;
+}
+
+
+sub _find_value_from_token_name {
+ my ( $key, $tokens ) = @_;
+ my $value = undef;
+
+ my @elements = split /\./, $key;
+ foreach my $e (@elements) {
+ if ( not defined $value ) {
+ $value = $tokens->{$e};
+ }
+ elsif ( is_plain_hashref($value) ) {
+ $value = $value->{$e};
+ }
+ elsif ( ref($value) ) {
+ local $@;
+ eval { $value = $value->$e };
+ $value = "" if $@;
+ }
+ }
+ return $value;
+}
+
+sub _interpolate_value {
+ my ($value) = @_;
+ if ( is_coderef($value) ) {
+ local $@;
+ eval { $value = $value->() };
+ $value = "" if $@;
+ }
+ elsif ( is_arrayref($value) ) {
+ $value = "@{$value}";
+ }
+
+ $value = "" if not defined $value;
+ return $value;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Template::Simple - Pure Perl 5 template engine for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+To use this engine, you may configure L<Dancer2> via C<config.yaml>:
+
+ template: simple
+
+=head1 DESCRIPTION
+
+This template engine is provided as a default one for the Dancer2 micro
+framework.
+
+This template engine should be fine for development purposes but is not a
+powerful one, it's written in pure Perl and has no C bindings to accelerate the
+template processing.
+
+If you want to power an application with Dancer2 in production environment, it's
+strongly advised to switch to L<Dancer2::Template::TemplateToolkit>.
+
+=head1 METHODS
+
+=head2 render($template, \%tokens)
+
+Renders the template. The first arg is a filename for the template file
+or a reference to a string that contains the template. The second arg
+is a hashref for the tokens that you wish to pass to
+L<Template::Toolkit> for rendering.
+
+=head1 SYNTAX
+
+A template written for C<Dancer2::Template::Simple> should be working just fine
+with L<Dancer2::Template::TemplateToolkit>. The opposite is not true though.
+
+=over 4
+
+=item B<variables>
+
+To interpolate a variable in the template, use the following syntax:
+
+ <% var1 %>
+
+If B<var1> exists in the tokens hash given, its value will be written there.
+
+=back
+
+=head1 SEE ALSO
+
+L<Dancer2>, L<Dancer2::Core::Role::Template>,
+L<Dancer2::Template::TemplateToolkit>.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Template/TemplateToolkit.pm b/lib/Dancer2/Template/TemplateToolkit.pm
new file mode 100644
index 00000000..f5ab9560
--- /dev/null
+++ b/lib/Dancer2/Template/TemplateToolkit.pm
@@ -0,0 +1,245 @@
+# ABSTRACT: Template toolkit engine for Dancer2
+
+package Dancer2::Template::TemplateToolkit;
+$Dancer2::Template::TemplateToolkit::VERSION = '0.300000';
+use Moo;
+use Carp qw<croak>;
+use Dancer2::Core::Types;
+use Dancer2::FileUtils qw<path>;
+use Scalar::Util ();
+use Template;
+
+with 'Dancer2::Core::Role::Template';
+
+has '+engine' => ( isa => InstanceOf ['Template'], );
+
+sub _build_engine {
+ my $self = shift;
+ my $charset = $self->charset;
+ my %tt_config = (
+ ANYCASE => 1,
+ ABSOLUTE => 1,
+ length($charset) ? ( ENCODING => $charset ) : (),
+ %{ $self->config },
+ );
+
+ my $start_tag = $self->config->{'start_tag'};
+ my $stop_tag = $self->config->{'stop_tag'} || $self->config->{end_tag};
+ $tt_config{'START_TAG'} = $start_tag
+ if defined $start_tag && $start_tag ne '[%';
+ $tt_config{'END_TAG'} = $stop_tag
+ if defined $stop_tag && $stop_tag ne '%]';
+
+ Scalar::Util::weaken( my $ttt = $self );
+ my $include_path = $self->config->{include_path};
+ $tt_config{'INCLUDE_PATH'} ||= [
+ ( defined $include_path ? $include_path : () ),
+ sub { [ $ttt->views ] },
+ ];
+
+ my $tt = Template->new(%tt_config);
+ $Template::Stash::PRIVATE = undef if $self->config->{show_private_variables};
+ return $tt;
+}
+
+sub render {
+ my ( $self, $template, $tokens ) = @_;
+
+ my $content = '';
+ my $charset = $self->charset;
+ my @options = length($charset) ? ( binmode => ":encoding($charset)" ) : ();
+ $self->engine->process( $template, $tokens, \$content, @options )
+ or croak 'Failed to render template: ' . $self->engine->error;
+
+ return $content;
+}
+
+# Override *_pathname methods from Dancer2::Core::Role::Template
+# Let TT2 do the concatenation of paths to template names.
+#
+# TT2 will look in a its INCLUDE_PATH for templates.
+# Typically $self->views is an absolute path, and we set ABSOLUTE=> 1 above.
+# In that case TT2 does NOT iterate through what is set for INCLUDE_PATH
+# However, if its not absolute, we want to allow TT2 iterate through the
+# its INCLUDE_PATH, which we set to be $self->views.
+
+sub view_pathname {
+ my ( $self, $view ) = @_;
+ return $self->_template_name($view);
+}
+
+sub layout_pathname {
+ my ( $self, $layout ) = @_;
+ return path(
+ $self->layout_dir,
+ $self->_template_name($layout),
+ );
+}
+
+sub pathname_exists {
+ my ( $self, $pathname ) = @_;
+ my $exists = eval {
+ # dies if pathname can not be found via TT2's INCLUDE_PATH search
+ $self->engine->service->context->template( $pathname );
+ 1;
+ };
+ $self->log_cb->( debug => $@ ) if ! $exists;
+ return $exists;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Template::TemplateToolkit - Template toolkit engine for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+To use this engine, you may configure L<Dancer2> via C<config.yaml>:
+
+ template: "template_toolkit"
+
+Or you may also change the rendering engine on a per-route basis by
+setting it manually with C<set>:
+
+ # code code code
+ set template => 'template_toolkit';
+
+Most configuration variables available when creating a new instance of a
+L<Template>::Toolkit object can be declared inside the template toolkit
+section on the engines configuration in your config.yml file. For example:
+
+ engines:
+ template:
+ template_toolkit:
+ start_tag: '<%'
+ end_tag: '%>'
+
+(Note: C<start_tag> and C<end_tag> are regexes. If you want to use PHP-style
+tags, you will need to list them as C<< <\? >> and C<< \?> >>.)
+See L<Template::Manual::Config> for the configuration variables.
+
+In addition to the standard configuration variables, the option C<show_private_variables>
+is also available. Template::Toolkit, by default, does not render private variables
+(the ones starting with an underscore). If in your project it gets easier to disable
+this feature than changing variable names, add this option to your configuration.
+
+ show_private_variables: true
+
+B<Warning:> Given the way Template::Toolkit implements this option, different Dancer2
+applications running within the same interpreter will share this option!
+
+=head1 DESCRIPTION
+
+This template engine allows you to use L<Template>::Toolkit in L<Dancer2>.
+
+=head1 METHODS
+
+=head2 render($template, \%tokens)
+
+Renders the template. The first arg is a filename for the template file
+or a reference to a string that contains the template. The second arg
+is a hashref for the tokens that you wish to pass to
+L<Template::Toolkit> for rendering.
+
+=head1 ADVANCED CUSTOMIZATION
+
+L<Template>::Toolkit allows you to replace certain parts, like the internal
+STASH (L<Template::Stash>). In order to do that, one usually passes an object of another
+implementation such as L<Template::Stash::AutoEscaping> into the constructor.
+
+Unfortunately that is not possible when you configure L<Template>::Toolkit from
+your Dancer2 configuration file. You cannot instantiate a Perl object in a yaml file.
+Instead, you need to subclass this module, and use the subclass in your configuration file.
+
+A subclass to use the aforementioned L<Template::Stash::AutoEscaping> might look like this:
+
+ package Dancer2::Template::TemplateToolkit::AutoEscaping;
+ # or MyApp::
+
+ use Moo;
+ use Template::Stash::AutoEscaping;
+
+ extends 'Dancer2::Template::TemplateToolkit';
+
+ around '_build_engine' => sub {
+ my $orig = shift;
+ my $self = shift;
+
+ my $tt = $self->$orig(@_);
+
+ # replace the stash object
+ $tt->service->context->{STASH} = Template::Stash::AutoEscaping->new(
+ $self->config->{STASH}
+ );
+
+ return $tt;
+ };
+
+ 1;
+
+You can then use this new subclass in your config file instead of C<template_toolkit>.
+
+ # in config.yml
+ engines:
+ template:
+ TemplateToolkit::AutoEscaping:
+ start_tag: '<%'
+ end_tag: '%>'
+ # optional arguments here
+ STASH:
+
+The same approach should work for SERVICE (L<Template::Service>), CONTEXT (L<Template::Context>),
+PARSER (L<Template::Parser>) and GRAMMAR (L<Template::Grammar>). If you intend to replace
+several of these components in your app, it is suggested to create an app-specific subclass
+that handles all of them at the same time.
+
+=head2 Template Caching
+
+L<Template>::Tookit templates can be cached by adding the C<COMPILE_EXT> property to your
+template configuration settings:
+
+ # in config.yml
+ engines:
+ template:
+ template_toolkit:
+ start_tag: '<%'
+ end_tag: '%>'
+ COMPILE_EXT: '.tcc' # cached file extension
+
+Template caching will avoid the need to re-parse template files or blocks each time they are
+used. Cached templates are automatically updated when you update the original template file.
+
+By default, cached templates are saved in the same directory as your template. To save
+cached templates in a different directory, you can set the C<COMPILE_DIR> property in your
+Dancer2 configuration file.
+
+Please see L<Template::Manual::Config/Caching_and_Compiling_Options> for further
+details and more caching options.
+
+=head1 SEE ALSO
+
+L<Dancer2>, L<Dancer2::Core::Role::Template>, L<Template::Toolkit>.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Template/Tiny.pm b/lib/Dancer2/Template/Tiny.pm
new file mode 100644
index 00000000..5062e3a5
--- /dev/null
+++ b/lib/Dancer2/Template/Tiny.pm
@@ -0,0 +1,119 @@
+package Dancer2::Template::Tiny;
+# ABSTRACT: Template::Tiny engine for Dancer2
+$Dancer2::Template::Tiny::VERSION = '0.300000';
+use Moo;
+use Carp qw/croak/;
+use Dancer2::Core::Types;
+use Dancer2::Template::Implementation::ForkedTiny;
+use Dancer2::FileUtils 'read_file_content';
+
+with 'Dancer2::Core::Role::Template';
+
+has '+engine' => (
+ isa => InstanceOf ['Dancer2::Template::Implementation::ForkedTiny']
+);
+
+sub _build_engine {
+ Dancer2::Template::Implementation::ForkedTiny->new( %{ $_[0]->config } );
+}
+
+sub render {
+ my ( $self, $template, $tokens ) = @_;
+
+ ( ref $template || -f $template )
+ or croak "$template is not a regular file or reference";
+
+ my $template_data =
+ ref $template
+ ? ${$template}
+ : read_file_content($template);
+
+ my $content;
+
+ $self->engine->process( \$template_data, $tokens, \$content, )
+ or die "Could not process template file '$template'";
+
+ return $content;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Template::Tiny - Template::Tiny engine for Dancer2
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+This template engine allows you to use L<Template::Tiny> in L<Dancer2>.
+
+L<Template::Tiny> is an implementation of a subset of L<Template::Toolkit> (the
+major parts) which takes much less memory and is faster. If you're only using
+the main functions of Template::Toolkit, you could use Template::Tiny. You can
+also seamlessly move back to Template::Toolkit whenever you want.
+
+However, Dancer2 uses a modified version of L<Template::Tiny>, which is L<Dancer2::Template::Implementation::ForkedTiny>. It adds 2 features :
+
+=over
+
+=item *
+
+opening and closing tag are now configurable
+
+=item *
+
+CodeRefs are evaluated and their results is inserted in the result.
+
+=back
+
+You can read more on L<Dancer2::Template::Implementation::ForkedTiny>.
+
+To use this engine, all you need to configure in your L<Dancer2>'s
+C<config.yaml>:
+
+ template: "tiny"
+
+Of course, you can also set this B<while> working using C<set>:
+
+ # code code code
+ set template => 'tiny';
+
+Since L<Dancer2> has internal support for a wrapper-like option with the
+C<layout> configuration option, you can have a L<Template::Toolkit>-like WRAPPER
+even though L<Template::Tiny> doesn't really support it.
+
+=head1 METHODS
+
+=head2 render($template, \%tokens)
+
+Renders the template. The first arg is a filename for the template file
+or a reference to a string that contains the template. The second arg
+is a hashref for the tokens that you wish to pass to
+L<Template::Toolkit> for rendering.
+
+=head1 SEE ALSO
+
+L<Dancer2>, L<Dancer2::Core::Role::Template>, L<Template::Tiny>,
+L<Dancer2::Template::Implementation::ForkedTiny>.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/lib/Dancer2/Test.pm b/lib/Dancer2/Test.pm
new file mode 100644
index 00000000..dfbb5002
--- /dev/null
+++ b/lib/Dancer2/Test.pm
@@ -0,0 +1,978 @@
+package Dancer2::Test;
+# ABSTRACT: Useful routines for testing Dancer2 apps
+$Dancer2::Test::VERSION = '0.300000';
+use strict;
+use warnings;
+
+use Carp qw<carp croak>;
+use Test::More;
+use Test::Builder;
+use URI::Escape;
+use Data::Dumper;
+use File::Temp;
+use Ref::Util qw<is_arrayref>;
+
+use parent 'Exporter';
+our @EXPORT = qw(
+ dancer_response
+
+ response_content_is
+ response_content_isnt
+ response_content_is_deeply
+ response_content_like
+ response_content_unlike
+
+ response_status_is
+ response_status_isnt
+
+ response_headers_include
+ response_headers_are_deeply
+
+ response_is_file
+
+ route_exists
+ route_doesnt_exist
+
+ is_pod_covered
+ route_pod_coverage
+
+);
+
+#dancer1 also has read_logs, response_redirect_location_is
+#cf. https://github.com/PerlDancer2/Dancer22/issues/25
+
+use Dancer2::Core::Dispatcher;
+use Dancer2::Core::Request;
+
+# singleton to store all the apps
+my $_dispatcher = Dancer2::Core::Dispatcher->new;
+
+# prevent deprecation warnings
+our $NO_WARN = 0;
+
+# can be called with the ($method, $path, $option) triplet,
+# or can be fed a request object directly, or can be fed
+# a single string, assumed to be [ GET => $string ]
+# or can be fed a response (which is passed through without
+# any modification)
+sub dancer_response {
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+
+ _find_dancer_apps_for_dispatcher();
+
+ # useful for the high-level tests
+ return $_[0] if ref $_[0] eq 'Dancer2::Core::Response';
+
+ my ( $request, $env ) =
+ ref $_[0] eq 'Dancer2::Core::Request'
+ ? _build_env_from_request(@_)
+ : _build_request_from_env(@_);
+
+ # override the set_request so it actually sets our request instead
+ {
+ ## no critic qw(TestingAndDebugging::ProhibitNoWarnings)
+ no warnings qw<redefine once>;
+ *Dancer2::Core::App::set_request = sub {
+ my $self = shift;
+ $self->_set_request( $request );
+ $_->set_request( $request ) for @{ $self->defined_engines };
+ };
+ }
+
+ # since the response is a PSGI response
+ # we create a Response object which was originally expected
+ my $psgi_response = $_dispatcher->dispatch($env);
+ return Dancer2::Core::Response->new(
+ status => $psgi_response->[0],
+ headers => $psgi_response->[1],
+ content => $psgi_response->[2][0],
+ );
+}
+
+
+
+sub _build_request_from_env {
+
+ # arguments can be passed as the triplet
+ # or as a arrayref, or as a simple string
+ my ( $method, $path, $options ) =
+ @_ > 1 ? @_
+ : is_arrayref($_[0]) ? @{ $_[0] }
+ : ( GET => $_[0], {} );
+
+ my $env = {
+ %ENV,
+ REQUEST_METHOD => uc($method),
+ PATH_INFO => $path,
+ QUERY_STRING => '',
+ 'psgi.url_scheme' => 'http',
+ SERVER_PROTOCOL => 'HTTP/1.0',
+ SERVER_NAME => 'localhost',
+ SERVER_PORT => 3000,
+ HTTP_HOST => 'localhost',
+ HTTP_USER_AGENT => "Dancer2::Test simulator v " . Dancer2->VERSION,
+ };
+
+ if ( defined $options->{params} ) {
+ my @params;
+ while ( my ( $p, $value ) = each %{ $options->{params} } ) {
+ if ( is_arrayref($value) ) {
+ for my $v (@$value) {
+ push @params,
+ uri_escape_utf8($p) . '=' . uri_escape_utf8($v);
+ }
+ }
+ else {
+ push @params,
+ uri_escape_utf8($p) . '=' . uri_escape_utf8($value);
+ }
+ }
+ $env->{QUERY_STRING} = join( '&', @params );
+ }
+
+ my $request = Dancer2::Core::Request->new( env => $env );
+
+ # body
+ $request->body( $options->{body} ) if exists $options->{body};
+
+ # headers
+ if ( $options->{headers} ) {
+ for my $header ( @{ $options->{headers} } ) {
+ my ( $name, $value ) = @{$header};
+ $request->header( $name => $value );
+ if ( $name =~ /^cookie$/i ) {
+ $env->{HTTP_COOKIE} = $value;
+ }
+ }
+ }
+
+ # files
+ if ( $options->{files} ) {
+ for my $file ( @{ $options->{files} } ) {
+ my $headers = $file->{headers};
+ $headers->{'Content-Type'} ||= 'text/plain';
+
+ my $temp = File::Temp->new();
+ if ( $file->{data} ) {
+ print $temp $file->{data};
+ close($temp);
+ }
+ else {
+ require File::Copy;
+ File::Copy::copy( $file->{filename}, $temp );
+ }
+
+ my $upload = Dancer2::Core::Request::Upload->new(
+ filename => $file->{filename},
+ size => -s $temp->filename,
+ tempname => $temp->filename,
+ headers => $headers,
+ );
+
+ ## keep temp_fh in scope so it doesn't get deleted too early
+ ## But will get deleted by the time the test is finished.
+ $upload->{temp_fh} = $temp;
+
+ $request->uploads->{ $file->{name} } = $upload;
+ }
+ }
+
+ # content-type
+ if ( $options->{content_type} ) {
+ $request->content_type( $options->{content_type} );
+ }
+
+ return ( $request, $env );
+}
+
+sub _build_env_from_request {
+ my ($request) = @_;
+
+ my $env = {
+ REQUEST_METHOD => $request->method,
+ PATH_INFO => $request->path,
+ QUERY_STRING => '',
+ 'psgi.url_scheme' => 'http',
+ SERVER_PROTOCOL => 'HTTP/1.0',
+ SERVER_NAME => 'localhost',
+ SERVER_PORT => 3000,
+ HTTP_HOST => 'localhost',
+ HTTP_USER_AGENT => "Dancer2::Test simulator v" . Dancer2->VERSION,
+ };
+
+ # TODO
+ if ( my $params = $request->{_query_params} ) {
+ my @params;
+ while ( my ( $p, $value ) = each %{$params} ) {
+ if ( is_arrayref($value) ) {
+ for my $v (@$value) {
+ push @params,
+ uri_escape_utf8($p) . '=' . uri_escape_utf8($v);
+ }
+ }
+ else {
+ push @params,
+ uri_escape_utf8($p) . '=' . uri_escape_utf8($value);
+ }
+ }
+ $env->{QUERY_STRING} = join( '&', @params );
+ }
+
+ # TODO files
+
+ return ( $request, $env );
+}
+
+sub response_status_is {
+ my ( $req, $status, $test_name ) = @_;
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+
+ $test_name ||= "response status is $status for " . _req_label($req);
+
+ my $response = dancer_response($req);
+
+ my $tb = Test::Builder->new;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ $tb->is_eq( $response->[0], $status, $test_name );
+}
+
+sub _find_route_match {
+ my ( $request, $env ) =
+ ref $_[0] eq 'Dancer2::Core::Request'
+ ? _build_env_from_request(@_)
+ : _build_request_from_env(@_);
+
+ for my $app (@{$_dispatcher->apps}) {
+ for my $route (@{$app->routes->{lc($request->method)}}) {
+ if ( $route->match($request) ) {
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+sub route_exists {
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+
+ my $tb = Test::Builder->new;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ $tb->ok( _find_route_match($_[0]), $_[1]);
+}
+
+sub route_doesnt_exist {
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+
+ my $tb = Test::Builder->new;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ $tb->ok( !_find_route_match($_[0]), $_[1]);
+}
+
+sub response_status_isnt {
+ my ( $req, $status, $test_name ) = @_;
+
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+
+ $test_name ||= "response status is not $status for " . _req_label($req);
+
+ my $response = dancer_response($req);
+
+ my $tb = Test::Builder->new;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ $tb->isnt_eq( $response->[0], $status, $test_name );
+}
+
+{
+ # Map comparison operator names to human-friendly ones
+ my %cmp_name = (
+ is_eq => "is",
+ isnt_eq => "is not",
+ like => "matches",
+ unlike => "doesn't match",
+ );
+
+ sub _cmp_response_content {
+ my ( $req, $want, $test_name, $cmp ) = @_;
+
+ if ( @_ == 3 ) {
+ $cmp = $test_name;
+ $test_name = $cmp_name{$cmp};
+ $test_name =
+ "response content $test_name $want for " . _req_label($req);
+ }
+
+ my $response = dancer_response($req);
+
+ my $tb = Test::Builder->new;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ $tb->$cmp( $response->[2][0], $want, $test_name );
+ }
+}
+
+sub response_content_is {
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ _cmp_response_content( @_, 'is_eq' );
+}
+
+sub response_content_isnt {
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ _cmp_response_content( @_, 'isnt_eq' );
+}
+
+sub response_content_like {
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ _cmp_response_content( @_, 'like' );
+}
+
+sub response_content_unlike {
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ _cmp_response_content( @_, 'unlike' );
+}
+
+sub response_content_is_deeply {
+ my ( $req, $matcher, $test_name ) = @_;
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+ $test_name ||= "response content looks good for " . _req_label($req);
+
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ my $response = _req_to_response($req);
+ is_deeply $response->[2][0], $matcher, $test_name;
+}
+
+sub response_is_file {
+ my ( $req, $test_name ) = @_;
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+ $test_name ||= "a file is returned for " . _req_label($req);
+
+ my $response = _get_file_response($req);
+ my $tb = Test::Builder->new;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ return $tb->ok( defined($response), $test_name );
+}
+
+sub response_headers_are_deeply {
+ my ( $req, $expected, $test_name ) = @_;
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+ $test_name ||= "headers are as expected for " . _req_label($req);
+
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+ my $response = dancer_response( _expand_req($req) );
+
+ is_deeply(
+ _sort_headers( $response->[1] ),
+ _sort_headers($expected), $test_name
+ );
+}
+
+sub response_headers_include {
+ my ( $req, $expected, $test_name ) = @_;
+ carp 'Dancer2::Test is deprecated, please use Plack::Test instead'
+ unless $NO_WARN;
+ $test_name ||= "headers include expected data for " . _req_label($req);
+ my $tb = Test::Builder->new;
+
+ my $response = dancer_response($req);
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ print STDERR "Headers are: "
+ . Dumper( $response->[1] )
+ . "\n Expected to find header: "
+ . Dumper($expected)
+ if !$tb->ok(
+ _include_in_headers( $response->[1], $expected ),
+ $test_name
+ );
+}
+
+sub route_pod_coverage {
+
+ require Pod::Simple::Search;
+ require Pod::Simple::SimpleTree;
+
+ my $all_routes = {};
+
+ foreach my $app ( @{ $_dispatcher->apps } ) {
+ my $routes = $app->routes;
+ my $available_routes = [];
+ foreach my $method ( sort { $b cmp $a } keys %$routes ) {
+ foreach my $r ( @{ $routes->{$method} } ) {
+
+ # we don't need pod coverage for head
+ next if $method eq 'head';
+ push @$available_routes, $method . ' ' . $r->spec_route;
+ }
+ }
+ ## copy dereferenced array
+ $all_routes->{ $app->name }{routes} = [@$available_routes]
+ if @$available_routes;
+
+ # Pod::Simple v3.30 excluded the current directory even when in @INC.
+ # include the current directory as a search path; its backwards compatible
+ # with previous version.
+ my $undocumented_routes = [];
+ my $file = Pod::Simple::Search->new->find( $app->name, '.' );
+ if ($file) {
+ $all_routes->{ $app->name }{has_pod} = 1;
+ my $parser = Pod::Simple::SimpleTree->new->parse_file($file);
+ my $pod_dataref = $parser->root;
+ my $found_routes = {};
+ for ( my $i = 0; $i < @$available_routes; $i++ ) {
+
+ my $r = $available_routes->[$i];
+ my $app_string = lc $r;
+ $app_string =~ s/\*/_REPLACED_STAR_/g;
+
+ for ( my $idx = 0; $idx < @$pod_dataref; $idx++ ) {
+ my $pod_part = $pod_dataref->[$idx];
+
+ next if !is_arrayref($pod_part);
+ foreach my $ref_part (@$pod_part) {
+ is_arrayref($ref_part)
+ and push @$pod_dataref, $ref_part;
+ }
+
+ my $pod_string = lc $pod_part->[2];
+ $pod_string =~ s/['|"|\s]+/ /g;
+ $pod_string =~ s/\s$//g;
+ $pod_string =~ s/\*/_REPLACED_STAR_/g;
+ if ( $pod_string =~ m/^$app_string$/ ) {
+ $found_routes->{$app_string} = 1;
+ next;
+ }
+ }
+ if ( !$found_routes->{$app_string} ) {
+ push @$undocumented_routes, $r;
+ }
+ }
+ }
+ else { ### no POD found
+ $all_routes->{ $app->name }{has_pod} = 0;
+ }
+ if (@$undocumented_routes) {
+ $all_routes->{ $app->name }{undocumented_routes} =
+ $undocumented_routes;
+ }
+ elsif ( !$all_routes->{ $app->name }{has_pod}
+ && @{ $all_routes->{ $app->name }{routes} } )
+ {
+ ## copy dereferenced array
+ $all_routes->{ $app->name }{undocumented_routes} =
+ [ @{ $all_routes->{ $app->name }{routes} } ];
+ }
+ }
+
+ return $all_routes;
+}
+
+sub is_pod_covered {
+ my ($test_name) = @_;
+
+ $test_name ||= "is pod covered";
+ my $route_pod_coverage = route_pod_coverage();
+
+ my $tb = Test::Builder->new;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ foreach my $app ( @{ $_dispatcher->apps } ) {
+ my %undocumented_route =
+ ( map { $_ => 1 }
+ @{ $route_pod_coverage->{ $app->name }{undocumented_routes} } );
+ $tb->subtest(
+ $app->name . $test_name,
+ sub {
+ foreach my $route (
+ @{ $route_pod_coverage->{ $app->name }{routes} } )
+ {
+ ok( !$undocumented_route{$route}, "$route is documented" );
+ }
+ }
+ );
+ }
+}
+
+sub import {
+ my ( $class, %options ) = @_;
+
+ my @applications;
+ if ( ref $options{apps} eq ref( [] ) ) {
+ @applications = @{ $options{apps} };
+ }
+ else {
+ my ( $caller, $script ) = caller;
+
+ # if no app is passed, assume the caller is one.
+ @applications = ($caller) if $caller->can('dancer_app');
+ }
+
+ # register the apps to the test dispatcher
+ $_dispatcher->apps( [ map {
+ $_->dancer_app->finish();
+ $_->dancer_app;
+ } @applications ] );
+
+ $class->export_to_level( 1, $class, @EXPORT );
+}
+
+# private
+
+sub _req_label {
+ my $req = shift;
+
+ return
+ ref $req eq 'Dancer2::Core::Response' ? 'response object'
+ : ref $req eq 'Dancer2::Core::Request'
+ ? join( ' ', map { $req->$_ } qw/ method path / )
+ : is_arrayref($req) ? join( ' ', @$req )
+ : "GET $req";
+}
+
+sub _expand_req {
+ my $req = shift;
+ return is_arrayref($req) ? @$req : ( 'GET', $req );
+}
+
+# Sort arrayref of headers (turn it into a list of arrayrefs, sort by the header
+# & value, then turn it back into an arrayref)
+sub _sort_headers {
+ my @originalheaders = @{ shift() }; # take a copy we can modify
+ my @headerpairs;
+ while ( my ( $header, $value ) = splice @originalheaders, 0, 2 ) {
+ push @headerpairs, [ $header, $value ];
+ }
+
+ # We have an array of arrayrefs holding header => value pairs; sort them by
+ # header then value, and return them flattened back into an arrayref
+ return [
+ map {@$_}
+ sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @headerpairs
+ ];
+}
+
+# make sure the given header sublist is included in the full headers array
+sub _include_in_headers {
+ my ( $full_headers, $expected_subset ) = @_;
+
+ # walk through all the expected header pairs, make sure
+ # they exist with the same value in the full_headers list
+ # return false as soon as one is not.
+ for ( my $i = 0; $i < scalar(@$expected_subset); $i += 2 ) {
+ my ( $name, $value ) =
+ ( $expected_subset->[$i], $expected_subset->[ $i + 1 ] );
+ return 0
+ unless _check_header( $full_headers, $name, $value );
+ }
+
+ # we've found all the expected pairs in the $full_headers list
+ return 1;
+}
+
+sub _check_header {
+ my ( $headers, $key, $value ) = @_;
+ for ( my $i = 0; $i < scalar(@$headers); $i += 2 ) {
+ my ( $name, $val ) = ( $headers->[$i], $headers->[ $i + 1 ] );
+ return 1 if $name eq $key && $value eq $val;
+ }
+ return 0;
+}
+
+sub _req_to_response {
+ my $req = shift;
+
+ # already a response object
+ return $req if ref $req eq 'Dancer2::Core::Response';
+
+ return dancer_response( is_arrayref($req) ? @$req : ( 'GET', $req ) );
+}
+
+# make sure we have at least one app in the dispatcher, and if not,
+# we must have at this point an app within the caller
+sub _find_dancer_apps_for_dispatcher {
+ return if scalar( @{ $_dispatcher->apps } );
+
+ for ( my $deep = 0; $deep < 5; $deep++ ) {
+ my $caller = caller($deep);
+ next if !$caller || !$caller->can('dancer_app');
+
+ return $_dispatcher->apps( [ $caller->dancer_app ] );
+ }
+
+ croak "Unable to find a Dancer2 app, did you use Dancer2 in your test?";
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Test - Useful routines for testing Dancer2 apps
+
+=head1 VERSION
+
+version 0.300000
+
+=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<DEPRECATED. This module and all the functions listed below are deprecated. Do
+not use this module.> The routines provided by this module for testing Dancer2
+apps are buggy and unnecessary. Instead, use the L<Plack::Test> module as shown
+in the SYNOPSIS above and ignore the functions in this documentation. Consult
+the L<Plack::Test> 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<Plack::Test> 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<NO_WARN> 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<data> 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<Dancer2::Handler::File> 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<Dancer2::Test/response_status_is> or L<Dancer2::Test/dancer_response>
+
+ 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<Dancer2::Handler::File>.
+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) 2019 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
diff --git a/lib/Dancer2/Tutorial.pod b/lib/Dancer2/Tutorial.pod
new file mode 100644
index 00000000..870fa66d
--- /dev/null
+++ b/lib/Dancer2/Tutorial.pod
@@ -0,0 +1,722 @@
+package Dancer2::Tutorial;
+# ABSTRACT: An example to get you dancing
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+Dancer2::Tutorial - An example to get you dancing
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 What is Dancer2?
+
+Dancer2 is a "micro" web framework which is modeled after a Ruby framework
+called L<Sinatra|http://www.sinatrarb.com> that constructs web applications
+by building a list of HTTP verbs, URLs (called routes) and methods to handle
+that type of traffic to that specific URL.
+
+ use Dancer2;
+
+ get '/' => sub {
+ return 'Hello World!';
+ };
+
+ start;
+
+This example shows a single HTTP verb "GET" followed by the root URL "/" and
+an anonymous subroutine which returns the string C<"Hello World!"> If you
+were to run this example, it would display "Hello World!" when you point
+your browser at L<http://localhost:3000>.
+
+=head1 How about a little more involved example?
+
+That's the reason I wrote this tutorial. While I was investigating some
+Python web frameworks like L<Flask|http://flask.pocoo.org/> or
+L<Bottle|https://bottlepy.org/docs/dev/> I enjoyed the way they
+explained step by step how to build an example application which was a
+little more involved than a trivial example.
+
+Using the
+L<Flaskr|https://github.com/pallets/flask>
+sample application as my inspiration (OK, shamelessly plagiarised) I
+translated that application to the Dancer2 framework so I could better
+understand how Dancer2 worked. (I'm learning it too!)
+
+So "Dancr" was born.
+
+Dancr is a simple "micro" blog which uses the
+L<SQLite|http://www.sqlite.org> database engine for simplicity's sake.
+(You'll need to install sqlite if you don't have it installed already.)
+
+=head1 Required perl modules
+
+Obviously you need L<Dancer2>. You also need the L<Template
+Toolkit|Template>, L<File::Slurper>, and L<DBD::SQLite>. These all can be
+installed using your CPAN client, as in:
+
+ cpan Dancer2 Template File::Slurper DBD::SQLite
+
+=head1 The database
+
+We're not going to spend a lot of time on the database, as it's not really
+the point of this particular tutorial. Open your favorite L<text
+editor|http://www.vim.org> and create a schema definition called
+'schema.sql' with the following content:
+
+ create table if not exists entries (
+ id integer primary key autoincrement,
+ title string not null,
+ text string not null
+ );
+
+Here we have a single table with three columns: id, title, and text. The
+'id' field is the primary key and will automatically get an ID assigned by
+the database engine when a row is inserted.
+
+We want our application to initialize the database automatically for us when
+we start it, so next, create a file called 'dancr.pl'. (The entire file is
+listed below, so don't worry about copying each of these fragments into
+'dancr.pl' as you read through this document.) We're going to put the
+following subroutines in that file:
+
+ sub connect_db {
+ my $dbh = DBI->connect("dbi:SQLite:dbname=".setting('database')) or
+ die $DBI::errstr;
+
+ return $dbh;
+ }
+
+ sub init_db {
+ my $db = connect_db();
+ my $schema = read_text('./schema.sql');
+ $db->do($schema) or die $db->errstr;
+ }
+
+Nothing too fancy in here, I hope. Standard DBI except for the
+C<setting('database')> thing - more on that in a bit. For now, just assume
+that the expression evaluates to the location of the database file.
+
+(Note that you may want to look at the L<Dancer2::Plugin::Database> module
+for an easy way to configure and manage database connections for your
+Dancer2 apps, but the above will suffice for this tutorial.)
+
+=head1 Our first route handler
+
+Let's tackle our first route handler now, the one for the root URL '/'. This
+is what it looks like:
+
+ get '/' => sub {
+ my $db = connect_db();
+ my $sql = 'select id, title, text from entries order by id desc';
+ my $sth = $db->prepare($sql) or die $db->errstr;
+ $sth->execute or die $sth->errstr;
+ template 'show_entries.tt', {
+ 'msg' => get_flash(),
+ 'add_entry_url' => uri_for('/add'),
+ 'entries' => $sth->fetchall_hashref('id'),
+ };
+ };
+
+As you can see, the handler is created by specifying the HTTP verb 'get',
+the '/' URL to match, and finally, a subroutine to do something once those
+conditions have been satisfied. Something you might not notice right away
+is the semicolon at the end of the route handler. Since the subroutine
+is actually a coderef, it requires a semicolon.
+
+Let's take a closer look at the subroutine. The first few lines are
+standard DBI. The only new concept as part of Dancer2 is that C<template>
+directive at the end of the handler. That tells Dancer2 to process the
+output through one of its templating engines. In this case, we're using
+L<Template Toolkit|Template> which offers a lot more flexibility than the
+simple default Dancer2 template engine.
+
+Templates all go into the C<views/> directory. Optionally, you can create a
+"layout" template which provides a consistent look and feel for all of your
+views. We'll construct our own layout template cleverly named F<main.tt> a
+little later in this tutorial.
+
+What's going on with the hashref as the second argument to the template
+directive? Those are all of the parameters we want to pass into our
+template. We have a C<msg> field which displays a message to the user when
+an event happens like a new entry is posted, or the user logs in or out.
+It's called a "flash" message because we only want to display it one time,
+not every time the / URL is rendered.
+
+The C<uri_for> directive tells Dancer2 to provide a URI for that specific
+route, in this case, it is the route to post a new entry into the database.
+You might ask why we don't simply hardcode the C</add> URI in our
+application or templates. The best reason B<not> to do that is because it
+removes a layer of flexibility as to where to "mount" the web application.
+Although the application is coded to use the root URL C</> it might be
+better in the future to locate it under its own URL route (maybe C</dancr>?)
+- at that point we'd have to go through our application and the templates
+and update the URLs and hope we didn't miss any of them. By using the
+C<uri_for> Dancer2 method, we can easily load the application wherever we
+like and not have to modify the application at all.
+
+Finally, the C<entries> field contains a hashref with the results from our
+database query. Those results will be rendered in the template itself, so
+we just pass them in.
+
+So what does the F<show_entries.tt> template look like? This:
+
+ [% IF session.logged_in %]
+ <form action="[% add_entry_url %]" method=post class=add-entry>
+ <dl>
+ <dt>Title:
+ <dd><input type=text size=30 name=title>
+ <dt>Text:
+ <dd><textarea name=text rows=5 cols=40></textarea>
+ <dd><input type=submit value=Share>
+ </dl>
+ </form>
+ [% END %]
+ <ul class=entries>
+ [% IF entries.size %]
+ [% FOREACH id IN entries.keys.nsort %]
+ <li><h2>[% entries.$id.title | html %]</h2>[% entries.$id.text | html %]
+ [% END %]
+ [% ELSE %]
+ <li><em>Unbelievable. No entries here so far</em>
+ [% END %]
+ </ul>
+
+Again, since this isn't a tutorial specifically about Template Toolkit, I'm
+going to gloss over the syntax here and just point out the section which
+starts with C<E<lt>ul class=entriesE<gt>> - this is the section where the
+database query results are displayed. You can also see at the very top some
+discussion about a session - more on that soon.
+
+The only other Template Toolkit related thing that has to be mentioned here is
+the C<| html> in C<[% entries.$id.title | html %]>. That's
+L<a filter|http://www.template-toolkit.org/docs/manual/Filters.html#section_html>
+to convert characters like C<E<lt>> and C<E<gt>> to C<&lt;> and C<&gt;>. This
+way they will be displayed by the browser as content on the page rather than
+just included. If we did not do this, the browser might interpret content as
+part of the page, and a malicious user could smuggle in all kinds of bad code
+that would then run in another user's browser. This is called
+L<Cross Site Scripting|https://en.wikipedia.org/wiki/Cross-site_scripting> or
+XSS and you should make sure to avoid it by always filtering data that came
+in from the web when you display it in a template.
+
+=head1 Other HTTP verbs
+
+There are 8 defined HTTP verbs defined in L<RFC
+2616|http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9>: OPTIONS,
+GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT. Of these, the majority of web
+applications focus on the verbs which closely map to the CRUD (Create,
+Retrieve, Update, Delete) operations most database-driven applications need
+to implement.
+
+In addition, the C<PATCH> verb was defined in
+L<RFC5789|http://tools.ietf.org/html/rfc5789>, and is intended as a "partial
+PUT" - sending just the changes required to the entity in question. How
+this would be handled is down to your app, it will vary depending on the
+type of entity in question and the serialization in use.
+
+Dancer2 currently supports GET, PUT/PATCH, POST, DELETE, OPTIONS which map
+to Retrieve, Update, Create, Delete respectively. Let's take a look now at
+the C</add> route handler which handles a POST operation.
+
+ post '/add' => sub {
+ if ( not session('logged_in') ) {
+ send_error("Not logged in", 401);
+ }
+
+ my $db = connect_db();
+ my $sql = 'insert into entries (title, text) values (?, ?)';
+ my $sth = $db->prepare($sql) or die $db->errstr;
+ $sth->execute(
+ body_parameters->get('title'),
+ body_parameters->get('text')
+ ) or die $sth->errstr;
+
+ set_flash('New entry posted!');
+ redirect '/';
+ };
+
+As before, the HTTP verb begins the handler, followed by the route, and a
+subroutine to do something - in this case, it will insert a new entry into
+the database.
+
+The first check in the subroutine is to make sure the user sending the data
+is logged in. If not, the application returns an error and stops
+processing. Otherwise, we have standard DBI stuff. Let me insert (heh, heh)
+a blatant plug here for always, always using parameterized INSERTs in your
+application SQL statements. It's the only way to be sure your application
+won't be vulnerable to SQL injection. (See L<http://www.bobby-tables.com>
+for correct INSERT examples in multiple languages.) Here we're using the
+C<body_parameters> convenience method to pull in the parameters in the current HTTP
+request. (You can see the 'title' and 'text' form parameters in the
+F<show_entries.tt> template above.) Those values are inserted into the
+database, then we set a flash message for the user and redirect her back to
+the root URL.
+
+It's worth mentioning that the "flash message" is not part of Dancer2, but a
+part of this specific application. We need to implement it ourself.
+
+ sub set_flash {
+ my $message = shift;
+
+ session flash => $message;
+ }
+
+ sub get_flash {
+ my $msg = session('flash');
+ session->delete('flash');
+
+ return $msg;
+ }
+
+We need a way to save our temporary message, and a way to get it back out.
+Since it is a temporary message that should only be shown on the page
+immediately following the one where the value was set, we need to delete it
+once it was read.
+
+=head1 Sessions and logins
+
+A good way to implement this "flash message" mechanic is to write the message
+to the user's session. The session stores data for a specific user for the
+whole time she uses the application. It persists over all requests this user
+makes. You can read more about how to use the session
+in L<our manual|Dancer2::Manual/"SESSIONS">.
+
+Dancer2 comes with a simple in-memory session manager out of the box. It
+supports a bunch of other session engines including YAML, memcached, browser
+cookies and others. For this application we're going to stick with the
+in-memory model which works great for development and tutorials, but won't
+persist across server restarts or scale very well in "real world" production
+scenarios.
+
+=head2 Configuration options
+
+To use sessions in our application, we have to tell Dancer2 to activate the
+session handler and initialize a session manager. To do that, we add some
+configuration directives toward the top of our 'dancr.pl' file. But there are
+more options than just the session engine we want to set.
+
+ set 'database' => File::Spec->catfile(File::Spec->tmpdir(), 'dancr.db');
+ set 'session' => 'Simple';
+ set 'template' => 'template_toolkit';
+ set 'logger' => 'console';
+ set 'log' => 'debug';
+ set 'show_errors' => 1;
+ set 'startup_info' => 1;
+ set 'warnings' => 1;
+
+Hopefully these are fairly self-explanatory. We want the Simple session
+engine, the Template Toolkit template engine, logging enabled (at the
+'debug' level with output to the console instead of a file), we want to show
+errors to the web browser, log access attempts and log Dancer2 warnings
+(instead of silently ignoring them).
+
+In a more sophisticated application you would want to put these
+configuration options into a configuration file, but for this tutorial,
+we're going to keep it simple. Dancer2 also supports the notion of
+application environments, meaning you can create a configuration file for
+your development instance, and another config file for the production
+environment (with things like debugging and showing errors disabled
+perhaps). Dancer2 also doesn't impose any limits on what parameters you can
+set using the C<set> syntax. For this application we're going to embed our
+single username and password into the application itself:
+
+ set 'username' => 'admin';
+ set 'password' => 'password';
+
+Hopefully no one will ever guess our clever password! Obviously, you will
+want a more sophisticated user authentication scheme in any sort of
+non-tutorial application but this is good enough for our purposes.
+
+=head2 Logging in
+
+Now that Dancr is configured to handle sessions, let's take a look at the
+URL handler for the C</login> route.
+
+ any ['get', 'post'] => '/login' => sub {
+ my $err;
+
+ if ( request->method() eq "POST" ) {
+ # process form input
+ if ( body_parameters->get('username') ne setting('username') ) {
+ $err = "Invalid username";
+ }
+ elsif ( body_parameters->get('password') ne setting('password') ) {
+ $err = "Invalid password";
+ }
+ else {
+ session 'logged_in' => true;
+ set_flash('You are logged in.');
+ return redirect '/';
+ }
+ }
+
+ # display login form
+ template 'login.tt', {
+ 'err' => $err,
+ };
+ };
+
+This is the first handler which accepts two different verb types, a GET for
+a human browsing to the URL and a POST for the browser to submit the user's
+input to the web application. Since we're handling two different verbs, we
+check to see what verb is in the request. If it's B<not> a POST, we drop
+down to the C<template> directive and display the F<login.tt> template:
+
+ <h2>Login</h2>
+ [% IF err %]<p class=error><strong>Error:</strong> [% err %][% END %]
+ <form action="[% login_url %]" method=post>
+ <dl>
+ <dt>Username:
+ <dd><input type=text name=username>
+ <dt>Password:
+ <dd><input type=password name=password>
+ <dd><input type=submit value=Login>
+ </dl>
+ </form>
+
+This is even simpler than our F<show_entries.tt> template - but wait -
+there's a C<login_url> template parameter and we're only passing in the
+C<err> parameter. Where's the missing parameter? It's being generated and
+sent to the template in a C<before_template_render> directive - we'll come
+back to that in a moment or two.
+
+So the user fills out the F<login.tt> template and submits it back to the
+C</login> route handler. We now check the user input against our
+application settings and if the input is incorrect, we alert the user, otherwise
+the application starts a session and sets the C<logged_in> session parameter
+to the C<true()> value. Dancer2 exports both a C<true()> and C<false()>
+convenience method which we use here. After that, it's another flash
+message and back to the root URL handler.
+
+=head2 Logging out
+
+And finally, we need a way to clear our user's session with the customary
+logout procedure.
+
+ get '/logout' => sub {
+ app->destroy_session;
+ set_flash('You are logged out.');
+ redirect '/';
+ };
+
+C<app-E<gt>destroy_session;> is Dancer2's way to remove a stored session.
+We notify the user she is logged out and route her back to the root URL once
+again.
+
+You might wonder how we can then set a value in the session in C<set_flash>,
+because we just destroyed the session.
+
+Destroying the session has removed the data from the persistence layer (which
+is the memory of our running application, because we are using the C<simple>
+session engine). If we write to I<the session> now, it will actually create
+a completely new session for our user. This new, empty session will have a new
+I<session ID>, which Dancer2 tells the user's browser about in the response.
+When the browser requests the root URL, it will send this new session ID to our
+application.
+
+=head1 Layout and static files
+
+We still have a missing puzzle piece or two. First, how can we use Dancer2
+to serve our CSS stylesheet? Second, where are flash messages displayed?
+Third, what about the C<before_template_render> directive?
+
+=head2 Serving static files
+
+In Dancer2, static files should go into the C<public/> directory, but in the
+application itself be sure to omit the C<public/> element from the path. For
+example, the stylesheet for Dancr lives in C<dancr/public/css/style.css> but
+is served from L<http://localhost:3000/css/style.css>.
+
+If you wanted to build a mostly static web site you could simply write route
+handlers like this one:
+
+ get '/' => sub {
+ send_file 'index.html';
+ };
+
+where index.html would live in your C<public/> directory.
+
+C<send_file> does exactly what it says: it loads a static file, then sends
+the contents of that file to the user.
+
+=head2 Layouts
+
+I mentioned near the beginning of this tutorial that it is possible to
+create a C<layout> template. In Dancr, that layout is called C<main> and
+it's set up by putting in a directive like this:
+
+ set layout => 'main';
+
+near the top of your web application. This tells Dancer2's template
+engine that it should look for a file called F<main.tt> in
+C<dancr/views/layouts/> and insert the calls from the C<template> directive
+into a template parameter called C<content>.
+
+For this web application, the layout template looks like this:
+
+ <!doctype html>
+ <html>
+ <head>
+ <title>Dancr</title>
+ <link rel=stylesheet type=text/css href="[% css_url %]">
+ </head>
+ <body>
+ <div class=page>
+ <h1>Dancr</h1>
+ <div class=metanav>
+ [% IF not session.logged_in %]
+ <a href="[% login_url %]">log in</a>
+ [% ELSE %]
+ <a href="[% logout_url %]">log out</a>
+ [% END %]
+ </div>
+ [% IF msg %]
+ <div class=flash> [% msg %] </div>
+ [% END %]
+ [% content %]
+ </div>
+ </body>
+ </html>
+
+Aha! You now see where the flash message C<msg> parameter gets rendered. You
+can also see where the content from the specific route handlers is inserted
+(the fourth line from the bottom in the C<content> template parameter).
+
+But what about all those other C<*_url> template parameters?
+
+=head2 Using C<before_template_render>
+
+Dancer2 has a way to manipulate the template parameters before they're
+passed to the engine for processing. It's C<before_template_render>. Using
+this directive, you can generate and set the URIs for the C</login> and
+C</logout> route handlers and the URI for the stylesheet. This is handy for
+situations like this where there are values which are re-used consistently
+across all (or most) templates. This cuts down on code-duplication and
+makes your app easier to maintain over time since you only need to update
+the values in this one place instead of everywhere you render a template.
+
+ hook before_template_render => sub {
+ my $tokens = shift;
+
+ $tokens->{'css_url'} = request->base . 'css/style.css';
+ $tokens->{'login_url'} = uri_for('/login');
+ $tokens->{'logout_url'} = uri_for('/logout');
+ };
+
+Here again I'm using C<uri_for> instead of hardcoding the routes. This code
+block is executed before any of the templates are processed so that the
+template parameters have the appropriate values before being rendered.
+
+=head1 Putting it all together
+
+Here's the complete 'dancr.pl' script from start to finish.
+
+ use Dancer2;
+ use DBI;
+ use File::Spec;
+ use File::Slurper qw/ read_text /;
+ use Template;
+
+ set 'database' => File::Spec->catfile(File::Spec->tmpdir(), 'dancr.db');
+ set 'session' => 'Simple';
+ set 'template' => 'template_toolkit';
+ set 'logger' => 'console';
+ set 'log' => 'debug';
+ set 'show_errors' => 1;
+ set 'startup_info' => 1;
+ set 'warnings' => 1;
+ set 'username' => 'admin';
+ set 'password' => 'password';
+ set 'layout' => 'main';
+
+ sub set_flash {
+ my $message = shift;
+
+ session flash => $message;
+ }
+
+ sub get_flash {
+ my $msg = session('flash');
+ session->delete('flash');
+
+ return $msg;
+ }
+
+ sub connect_db {
+ my $dbh = DBI->connect("dbi:SQLite:dbname=".setting('database')) or
+ die $DBI::errstr;
+
+ return $dbh;
+ }
+
+ sub init_db {
+ my $db = connect_db();
+ my $schema = read_text('./schema.sql');
+ $db->do($schema) or die $db->errstr;
+ }
+
+ hook before_template_render => sub {
+ my $tokens = shift;
+
+ $tokens->{'css_url'} = request->base . 'css/style.css';
+ $tokens->{'login_url'} = uri_for('/login');
+ $tokens->{'logout_url'} = uri_for('/logout');
+ };
+
+ get '/' => sub {
+ my $db = connect_db();
+ my $sql = 'select id, title, text from entries order by id desc';
+ my $sth = $db->prepare($sql) or die $db->errstr;
+ $sth->execute or die $sth->errstr;
+ template 'show_entries.tt', {
+ 'msg' => get_flash(),
+ 'add_entry_url' => uri_for('/add'),
+ 'entries' => $sth->fetchall_hashref('id'),
+ };
+ };
+
+ post '/add' => sub {
+ if ( not session('logged_in') ) {
+ send_error("Not logged in", 401);
+ }
+
+ my $db = connect_db();
+ my $sql = 'insert into entries (title, text) values (?, ?)';
+ my $sth = $db->prepare($sql) or die $db->errstr;
+ $sth->execute(
+ body_parameters->get('title'),
+ body_parameters->get('text')
+ ) or die $sth->errstr;
+
+ set_flash('New entry posted!');
+ redirect '/';
+ };
+
+ any ['get', 'post'] => '/login' => sub {
+ my $err;
+
+ if ( request->method() eq "POST" ) {
+ # process form input
+ if ( body_parameters->get('username') ne setting('username') ) {
+ $err = "Invalid username";
+ }
+ elsif ( body_parameters->get('password') ne setting('password') ) {
+ $err = "Invalid password";
+ }
+ else {
+ session 'logged_in' => true;
+ set_flash('You are logged in.');
+ return redirect '/';
+ }
+ }
+
+ # display login form
+ template 'login.tt', {
+ 'err' => $err,
+ };
+
+ };
+
+ get '/logout' => sub {
+ app->destroy_session;
+ set_flash('You are logged out.');
+ redirect '/';
+ };
+
+ init_db();
+ start;
+
+=head1 Advanced route moves
+
+There's a lot more to route matching than shown here. For example, you can
+match routes with regular expressions, or you can match pieces of a route
+like C</hello/:name> where the C<:name> piece magically turns into a named
+parameter in your handler for manipulation.
+
+=head1 Next Steps
+
+Hopefully this effort has been helpful and interesting enough to get you
+exploring Dancer2 on your own. Dancer2 is suitable for projects of all
+shapes and sizes, and you can easily build a solution tailored to your
+particular needs.
+
+There is certainly a lot left to cover. We suggest you take a stroll through
+the L<manual|Dancer2::Manual>for more detailed information about the
+topics covered in this tutorial, and for a more thorough explanation of all
+things Dancer2-related. The L<cookbook|Dancer2::Cookbook> will show you how
+to handle some handy tips and tricks when working with Dancer2. Additionally,
+there are a lot of great L<plugins|https://metacpan.org/search?q=Dancer2%3A%3APlugin>
+which extend and enhance the capabilities of the framework.
+
+Happy dancing!
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<http://perldancer.org>
+
+=item *
+
+L<http://github.com/PerlDancer/Dancer2>
+
+=item *
+
+L<Dancer2::Plugins>
+
+=back
+
+=head1 CSS COPYRIGHT AND LICENSE
+
+The CSS stylesheet is copied verbatim from the Flaskr example application
+and is subject to their license:
+
+Copyright (c) 2010, 2013 by Armin Ronacher and contributors.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms of the software as well as
+documentation, with or without modification, are permitted provided that the
+following conditions are met:
+
+=over 4
+
+=item *
+
+Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+=item *
+
+Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+=item *
+
+The names of the contributors may not be used to endorse or promote products
+derived from this software without specific prior written permission.
+
+=back
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/script/dancer2 b/script/dancer2
new file mode 100755
index 00000000..503ea67a
--- /dev/null
+++ b/script/dancer2
@@ -0,0 +1,208 @@
+#!/usr/bin/perl
+# PODNAME: dancer2
+# ABSTRACT: Dancer2 command line interface
+
+use strict;
+use warnings;
+use Dancer2::CLI;
+
+# backward compatibility
+if (@ARGV && ($ARGV[0] =~ m/^-(a|p|x|s)/ || $ARGV[0] =~ m/^--(application|path|no-check|skel)/)) {
+ # GetOptions and Getopt::Long::Descriptive differently treats
+ # cases like '-a=Test'. GetOptions returs 'Test' as value of 'a',
+ # while Getopt::Long::Descriptive returns '=Test' as value
+ foreach (@ARGV) {
+ s/^\-(a|p)=/-$1/;
+ }
+
+ unshift @ARGV, 'gen';
+}
+
+
+exit Dancer2::CLI->run;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+dancer2 - Dancer2 command line interface
+
+=head1 VERSION
+
+version 0.300000
+
+=head1 SYNOPSIS
+
+dancer2 <command> [options...]
+
+=head1 DESCRIPTION
+
+Dancer2 is the new generation lightweight web-framework for Perl.
+This tool provides nice, easily-extendable CLI interface for it.
+
+=head2 Documentation Index
+
+Documentation on Dancer2 is split into several manpages. Below is a
+complete outline on where to go for help.
+
+=over 4
+
+=item * Dancer2 Tutorial
+
+If you are new to the Dancer approach, you should start by reading
+our L<Dancer2::Tutorial>.
+
+=item * Dancer2 Manual
+
+L<Dancer2::Manual> 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<DSL Keywords|Dancer2::Manual/DSL KEYWORDS>.
+
+=item * Dancer2 Deployment
+
+For configuration examples of different deployment solutions involving
+Dancer2 and Plack, refer to L<Dancer2::Manual::Deployment>.
+
+=item * Dancer2 Cookbook
+
+Specific examples of code for real-life problems and some 'tricks' for
+applications in Dancer can be found in L<Dancer2::Cookbook>
+
+=item * Dancer2 Config
+
+For configuration file details refer to L<Dancer2::Config>. It is a
+complete list of all configuration options.
+
+=item * Dancer2 Plugins
+
+Refer to L<Dancer2::Plugins> 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<Dancer2::Manual::Migration> provides the most up-to-date instruction on
+how to convert a Dancer (1) based application to Dancer2.
+
+=back
+
+=head1 NAME
+
+dancer2 - Dancer2 command line interface
+
+=head1 COMMANDS
+
+=over
+
+=item gen : create new Dancer2 application
+
+=item commands : list the application's commands
+
+=item help : display a command's help screen
+
+=item version : display version
+
+=back
+
+To get detailed description of each individual command run:
+ dancer2 help <command>
+
+The lastest list of available commands can be dispayed by:
+ dancer2 commands
+
+=head1 COMMAND 'gen'
+
+Helper script for providing a bootstrapping method to quickly and easily create
+the framework for a new Dancer2 application.
+
+=head3 OPTIONS
+
+ -a --application the name of your application
+ -p --path the path where to create your application
+ (current directory if not specified)
+ -o --overwrite overwrite existing files
+ -x --no-check don't check for the latest version of Dancer2
+ (checking version implies internet connection)
+ -s --skel skeleton directory
+
+=head3 EXAMPLE
+
+Here is an application created with dancer2:
+
+ $ dancer2 gen -a MyWeb::App
+ + MyWeb-App
+ + MyWeb-App/bin
+ + MyWeb-App/bin/app.psgi
+ + MyWeb-App/config.yml
+ + MyWeb-App/environments
+ + MyWeb-App/environments/development.yml
+ + MyWeb-App/environments/production.yml
+ + MyWeb-App/views
+ + MyWeb-App/views/index.tt
+ + MyWeb-App/views/layouts
+ + MyWeb-App/views/layouts/main.tt
+ + MyWeb-App/MANIFEST.SKIP
+ + MyWeb-App/lib
+ + MyWeb-App/lib/MyWeb
+ + MyWeb-App/lib/MyWeb/App.pm
+ + MyWeb-App/public
+ + MyWeb-App/public/css
+ + MyWeb-App/public/css/style.css
+ + MyWeb-App/public/css/error.css
+ + MyWeb-App/public/images
+ + MyWeb-App/public/500.html
+ + MyWeb-App/public/404.html
+ + MyWeb-App/public/dispatch.fcgi
+ + MyWeb-App/public/dispatch.cgi
+ + MyWeb-App/public/javascripts
+ + MyWeb-App/public/javascripts/jquery.js
+ + MyWeb-App/t
+ + MyWeb-App/t/002_index_route.t
+ + MyWeb-App/t/001_base.t
+ + MyWeb-App/Makefile.PL
+
+The application is ready to serve:
+
+ $ cd MyWeb-App
+ $ plackup bin/app.psgi
+ >> Listening on 127.0.0.1:3000
+ == Entering the development dance floor ...
+
+=head1 AUTHOR
+
+This script has been written by Ivan Kruglov
+<ivan.kruglov@yahoo.com> base on original dancer2
+script which has been written by Sebastien Deseille
+<sebastien.deseille@gmail.com> and Alexis Sukrieh
+<sukria@cpan.org>.
+
+=head1 SOURCE CODE
+
+See L<Dancer2> for more information.
+
+=head1 LICENSE
+
+This module is free software and is published under the same
+terms as Perl itself.
+
+=head1 AUTHOR
+
+Dancer Core Developers
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2019 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
diff --git a/share/skel/.dancer b/share/skel/.dancer
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/share/skel/.dancer
diff --git a/share/skel/MANIFEST.SKIP b/share/skel/MANIFEST.SKIP
new file mode 100644
index 00000000..ed99aede
--- /dev/null
+++ b/share/skel/MANIFEST.SKIP
@@ -0,0 +1,16 @@
+^\.git\/
+maint
+^tags$
+.last_cover_stats
+Makefile$
+^blib
+^pm_to_blib
+^.*.bak
+^.*.old
+^t.*sessions
+^cover_db
+^.*\.log
+^.*\.swp$
+MYMETA.*
+^.gitignore
+^.svn\/
diff --git a/share/skel/Makefile.PL b/share/skel/Makefile.PL
new file mode 100644
index 00000000..1012ec73
--- /dev/null
+++ b/share/skel/Makefile.PL
@@ -0,0 +1,26 @@
+use strict;
+use warnings;
+use ExtUtils::MakeMaker;
+
+# Normalize version strings like 6.30_02 to 6.3002,
+# so that we can do numerical comparisons on it.
+my $eumm_version = $ExtUtils::MakeMaker::VERSION;
+$eumm_version =~ s/_//;
+
+WriteMakefile(
+ NAME => '[d2% appname %2d]',
+ AUTHOR => q{YOUR NAME <youremail@example.com>},
+ VERSION_FROM => '[d2% appfile %2d]',
+ ABSTRACT => 'YOUR APPLICATION ABSTRACT',
+ ($eumm_version >= 6.3001
+ ? ('LICENSE'=> 'perl')
+ : ()),
+ PL_FILES => {},
+ PREREQ_PM => {
+ 'Test::More' => 0,
+ 'YAML' => 0,
+ 'Dancer2' => [d2% dancer_version %2d],
+ },
+ dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
+ clean => { FILES => '[d2% cleanfiles %2d]-*' },
+);
diff --git a/share/skel/bin/+app.psgi b/share/skel/bin/+app.psgi
new file mode 100644
index 00000000..b9015be9
--- /dev/null
+++ b/share/skel/bin/+app.psgi
@@ -0,0 +1,45 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+
+
+# use this block if you don't need middleware, and only have a single target Dancer app to run here
+use [d2% appname %2d];
+
+[d2% appname %2d]->to_app;
+
+=begin comment
+# use this block if you want to include middleware such as Plack::Middleware::Deflater
+
+use [d2% appname %2d];
+use Plack::Builder;
+
+builder {
+ enable 'Deflater';
+ [d2% appname %2d]->to_app;
+}
+
+=end comment
+
+=cut
+
+=begin comment
+# use this block if you want to mount several applications on different path
+
+use [d2% appname %2d];
+use [d2% appname %2d]_admin;
+
+use Plack::Builder;
+
+builder {
+ mount '/' => [d2% appname %2d]->to_app;
+ mount '/admin' => [d2% appname %2d]_admin->to_app;
+}
+
+=end comment
+
+=cut
+
diff --git a/share/skel/config.yml b/share/skel/config.yml
new file mode 100644
index 00000000..7240c8a3
--- /dev/null
+++ b/share/skel/config.yml
@@ -0,0 +1,62 @@
+# This is the main configuration file of your Dancer2 app
+# env-related settings should go to environments/$env.yml
+# all the settings in this file will be loaded at Dancer's startup.
+
+# === Basic configuration ===
+
+# Your application's name
+appname: "[d2% appname %2d]"
+
+# The default layout to use for your application (located in
+# views/layouts/main.tt)
+layout: "main"
+
+# when the charset is set to UTF-8 Dancer2 will handle for you
+# all the magic of encoding and decoding. You should not care
+# about unicode within your app when this setting is set (recommended).
+charset: "UTF-8"
+
+# === Engines ===
+#
+# NOTE: All the engine configurations need to be under a single "engines:"
+# key. If you uncomment engine configurations below, make sure to delete
+# all "engines:" lines except the first. Otherwise, only the last
+# "engines:" block will take effect.
+
+# template engine
+# simple: default and very basic template engine
+# template_toolkit: TT
+
+template: "simple"
+
+# template: "template_toolkit"
+# engines:
+# template:
+# template_toolkit:
+# # Note: start_tag and end_tag are regexes
+# start_tag: '<%'
+# end_tag: '%>'
+
+# session engine
+#
+# Simple: in-memory session store - Dancer2::Session::Simple
+# YAML: session stored in YAML files - Dancer2::Session::YAML
+#
+# Check out metacpan for other session storage options:
+# https://metacpan.org/search?q=Dancer2%3A%3ASession&search_type=modules
+#
+# Default value for 'cookie_name' is 'dancer.session'. If you run multiple
+# Dancer apps on the same host then you will need to make sure 'cookie_name'
+# is different for each app.
+#
+#engines:
+# session:
+# Simple:
+# cookie_name: testapp.session
+#
+#engines:
+# session:
+# YAML:
+# cookie_name: eshop.session
+# is_secure: 1
+# is_http_only: 1
diff --git a/share/skel/cpanfile b/share/skel/cpanfile
new file mode 100644
index 00000000..231bc585
--- /dev/null
+++ b/share/skel/cpanfile
@@ -0,0 +1,11 @@
+requires "Dancer2" => "[d2% dancer_version %2d]";
+
+recommends "YAML" => "0";
+recommends "URL::Encode::XS" => "0";
+recommends "CGI::Deurl::XS" => "0";
+recommends "HTTP::Parser::XS" => "0";
+
+on "test" => sub {
+ requires "Test::More" => "0";
+ requires "HTTP::Request::Common" => "0";
+};
diff --git a/share/skel/environments/development.yml b/share/skel/environments/development.yml
new file mode 100644
index 00000000..096e5618
--- /dev/null
+++ b/share/skel/environments/development.yml
@@ -0,0 +1,23 @@
+# configuration file for development environment
+
+# the logger engine to use
+# console: log messages to STDOUT (your console where you started the
+# application server)
+# file: log message to a file in log/
+logger: "console"
+
+# the log level for this environment
+# core is the lowest, it shows Dancer2's core log messages as well as yours
+# (debug, info, warning and error)
+log: "core"
+
+# should Dancer2 consider warnings as critical errors?
+warnings: 1
+
+# should Dancer2 show a stacktrace when an 5xx error is caught?
+# if set to yes, public/500.html will be ignored and either
+# views/500.tt, 'error_template' template, or a default error template will be used.
+show_errors: 1
+
+# print the banner
+startup_info: 1
diff --git a/share/skel/environments/production.yml b/share/skel/environments/production.yml
new file mode 100644
index 00000000..41b436fc
--- /dev/null
+++ b/share/skel/environments/production.yml
@@ -0,0 +1,16 @@
+# configuration file for production environment
+
+# only log warning and error messsages
+log: "warning"
+
+# log message to a file in logs/
+logger: "file"
+
+# don't consider warnings critical
+warnings: 0
+
+# hide errors
+show_errors: 0
+
+# disable server tokens in production environments
+no_server_tokens: 1
diff --git a/share/skel/lib/AppFile.pm b/share/skel/lib/AppFile.pm
new file mode 100644
index 00000000..87b2ab77
--- /dev/null
+++ b/share/skel/lib/AppFile.pm
@@ -0,0 +1,10 @@
+package [d2% appname %2d];
+use Dancer2;
+
+our $VERSION = '0.1';
+
+get '/' => sub {
+ template 'index' => { 'title' => '[d2% appname %2d]' };
+};
+
+true;
diff --git a/share/skel/public/+dispatch.cgi b/share/skel/public/+dispatch.cgi
new file mode 100755
index 00000000..b5c2bd97
--- /dev/null
+++ b/share/skel/public/+dispatch.cgi
@@ -0,0 +1,16 @@
+[d2% perl_interpreter %2d]
+BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';}
+use Dancer2;
+use FindBin '$RealBin';
+use Plack::Runner;
+
+# For some reason Apache SetEnv directives don't propagate
+# correctly to the dispatchers, so forcing PSGI and env here
+# is safer.
+set apphandler => 'PSGI';
+set environment => 'production';
+
+my $psgi = path($RealBin, '..', 'bin', 'app.psgi');
+die "Unable to read startup script: $psgi" unless -r $psgi;
+
+Plack::Runner->run($psgi);
diff --git a/share/skel/public/+dispatch.fcgi b/share/skel/public/+dispatch.fcgi
new file mode 100644
index 00000000..51717e54
--- /dev/null
+++ b/share/skel/public/+dispatch.fcgi
@@ -0,0 +1,18 @@
+[d2% perl_interpreter %2d]
+BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';}
+use Dancer2;
+use FindBin '$RealBin';
+use Plack::Handler::FCGI;
+
+# For some reason Apache SetEnv directives don't propagate
+# correctly to the dispatchers, so forcing PSGI and env here
+# is safer.
+set apphandler => 'PSGI';
+set environment => 'production';
+
+my $psgi = path($RealBin, '..', 'bin', 'app.psgi');
+my $app = do($psgi);
+die "Unable to read startup script: $@" if $@;
+my $server = Plack::Handler::FCGI->new(nproc => 5, detach => 1);
+
+$server->run($app);
diff --git a/share/skel/public/404.html b/share/skel/public/404.html
new file mode 100644
index 00000000..c2f40326
--- /dev/null
+++ b/share/skel/public/404.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+ <title>Error 404</title>
+ <link rel="stylesheet" href="/css/error.css">
+</head>
+<body>
+<h1>Error 404</h1>
+<div id="content">
+<h2>Page Not Found</h2><p>Sorry, this is the void.</p>
+</div>
+<div id="footer">
+Powered by <a href="http://perldancer.org/">Dancer2</a>.
+</div>
+</body>
+</html>
diff --git a/share/skel/public/500.html b/share/skel/public/500.html
new file mode 100644
index 00000000..d84ebbc4
--- /dev/null
+++ b/share/skel/public/500.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+ <title>Error 500</title>
+ <link rel="stylesheet" href="/css/error.css">
+</head>
+<body>
+<h1>Error 500</h1>
+<div id="content">
+<h2>Internal Server Error</h2><p>Wooops, something went wrong</p>
+</div>
+<div id="footer">
+Powered by <a href="http://perldancer.org/">Dancer2</a>.
+</div>
+</body>
+</html>
diff --git a/share/skel/public/css/error.css b/share/skel/public/css/error.css
new file mode 100644
index 00000000..8a5e8317
--- /dev/null
+++ b/share/skel/public/css/error.css
@@ -0,0 +1,85 @@
+body {
+ font-family: Lucida,sans-serif;
+}
+
+h1 {
+ color: #AA0000;
+ border-bottom: 1px solid #444;
+}
+
+h2 { color: #444; }
+
+pre {
+ font-family: "lucida console","monaco","andale mono","bitstream vera sans mono","consolas",monospace;
+ font-size: 12px;
+ border-left: 2px solid #777;
+ padding-left: 1em;
+}
+
+footer {
+ font-size: 10px;
+}
+
+span.key {
+ color: #449;
+ font-weight: bold;
+ width: 120px;
+ display: inline;
+}
+
+span.value {
+ color: #494;
+}
+
+/* these are for the message boxes */
+
+pre.content {
+ background-color: #eee;
+ color: #000;
+ padding: 1em;
+ margin: 0;
+ border: 1px solid #aaa;
+ border-top: 0;
+ margin-bottom: 1em;
+ overflow-x: auto;
+}
+
+div.title {
+ font-family: "lucida console","monaco","andale mono","bitstream vera sans mono","consolas",monospace;
+ font-size: 12px;
+ background-color: #aaa;
+ color: #444;
+ font-weight: bold;
+ padding: 3px;
+ padding-left: 10px;
+}
+
+table.context {
+ border-spacing: 0;
+}
+
+table.context th, table.context td {
+ padding: 0;
+}
+
+table.context th {
+ color: #889;
+ font-weight: normal;
+ padding-right: 15px;
+ text-align: right;
+}
+
+.errline {
+ color: red;
+}
+
+pre.error {
+ background: #334;
+ color: #ccd;
+ padding: 1em;
+ border-top: 1px solid #000;
+ border-left: 1px solid #000;
+ border-right: 1px solid #eee;
+ border-bottom: 1px solid #eee;
+}
+
diff --git a/share/skel/public/css/style.css b/share/skel/public/css/style.css
new file mode 100644
index 00000000..80f94ebe
--- /dev/null
+++ b/share/skel/public/css/style.css
@@ -0,0 +1,189 @@
+
+body {
+margin: 0;
+margin-bottom: 25px;
+padding: 0;
+background-color: #ddd;
+background-image: url("/images/perldancer-bg.jpg");
+background-repeat: no-repeat;
+background-position: top left;
+
+font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
+font-size: 13px;
+color: #333;
+}
+
+h1 {
+font-size: 28px;
+color: #000;
+}
+
+a {color: #03c}
+a:hover {
+background-color: #03c;
+color: white;
+text-decoration: none;
+}
+
+#page {
+background-color: #ddd;
+width: 750px;
+margin: auto;
+margin-left: auto;
+padding-left: 0px;
+margin-right: auto;
+}
+
+#content {
+background-color: white;
+border: 3px solid #aaa;
+border-top: none;
+padding: 25px;
+width: 500px;
+}
+
+#sidebar {
+float: right;
+width: 175px;
+}
+
+#header, #about, #getting-started {
+padding-left: 75px;
+padding-right: 30px;
+}
+
+
+#header {
+background-image: url("/images/perldancer.jpg");
+background-repeat: no-repeat;
+background-position: top left;
+height: 64px;
+}
+#header h1, #header h2 {margin: 0}
+#header h2 {
+color: #888;
+font-weight: normal;
+font-size: 16px;
+}
+
+#about h3 {
+margin: 0;
+margin-bottom: 10px;
+font-size: 14px;
+}
+
+#about-content {
+background-color: #ffd;
+border: 1px solid #fc0;
+margin-left: -11px;
+}
+#about-content table {
+margin-top: 10px;
+margin-bottom: 10px;
+font-size: 11px;
+border-collapse: collapse;
+}
+#about-content td {
+padding: 10px;
+padding-top: 3px;
+padding-bottom: 3px;
+}
+#about-content td.name {color: #555}
+#about-content td.value {color: #000}
+
+#about-content.failure {
+background-color: #fcc;
+border: 1px solid #f00;
+}
+#about-content.failure p {
+margin: 0;
+padding: 10px;
+}
+
+#getting-started {
+border-top: 1px solid #ccc;
+margin-top: 25px;
+padding-top: 15px;
+}
+#getting-started h1 {
+margin: 0;
+font-size: 20px;
+}
+#getting-started h2 {
+margin: 0;
+font-size: 14px;
+font-weight: normal;
+color: #333;
+margin-bottom: 25px;
+}
+#getting-started ol {
+margin-left: 0;
+padding-left: 0;
+}
+#getting-started li {
+font-size: 18px;
+color: #888;
+margin-bottom: 25px;
+}
+#getting-started li h2 {
+margin: 0;
+font-weight: normal;
+font-size: 18px;
+color: #333;
+}
+#getting-started li p {
+color: #555;
+font-size: 13px;
+}
+
+#search {
+margin: 0;
+padding-top: 10px;
+padding-bottom: 10px;
+font-size: 11px;
+}
+#search input {
+font-size: 11px;
+margin: 2px;
+}
+#search-text {width: 170px}
+
+#sidebar ul {
+margin-left: 0;
+padding-left: 0;
+}
+#sidebar ul h3 {
+margin-top: 25px;
+font-size: 16px;
+padding-bottom: 10px;
+border-bottom: 1px solid #ccc;
+}
+#sidebar li {
+list-style-type: none;
+}
+#sidebar ul.links li {
+margin-bottom: 5px;
+}
+
+h1, h2, h3, h4, h5 {
+font-family: sans-serif;
+margin: 1.2em 0 0.6em 0;
+}
+
+p {
+line-height: 1.5em;
+margin: 1.6em 0;
+}
+
+code, .filepath, .app-info {
+ font-family: 'Andale Mono', Monaco, 'Liberation Mono', 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', monospace;
+}
+
+#footer {
+clear: both;
+padding-top: 2em;
+text-align: center;
+padding-right: 160px;
+font-family: sans-serif;
+font-size: 10px;
+}
diff --git a/share/skel/public/favicon.ico b/share/skel/public/favicon.ico
new file mode 100644
index 00000000..96c74653
--- /dev/null
+++ b/share/skel/public/favicon.ico
Binary files differ
diff --git a/share/skel/public/images/perldancer-bg.jpg b/share/skel/public/images/perldancer-bg.jpg
new file mode 100644
index 00000000..6ee6b773
--- /dev/null
+++ b/share/skel/public/images/perldancer-bg.jpg
Binary files differ
diff --git a/share/skel/public/images/perldancer.jpg b/share/skel/public/images/perldancer.jpg
new file mode 100644
index 00000000..3f9e718c
--- /dev/null
+++ b/share/skel/public/images/perldancer.jpg
Binary files differ
diff --git a/share/skel/t/001_base.t b/share/skel/t/001_base.t
new file mode 100644
index 00000000..c876c6ba
--- /dev/null
+++ b/share/skel/t/001_base.t
@@ -0,0 +1,5 @@
+use strict;
+use warnings;
+
+use Test::More tests => 1;
+use_ok '[d2% appname %2d]';
diff --git a/share/skel/t/002_index_route.t b/share/skel/t/002_index_route.t
new file mode 100644
index 00000000..16af073d
--- /dev/null
+++ b/share/skel/t/002_index_route.t
@@ -0,0 +1,16 @@
+use strict;
+use warnings;
+
+use [d2% appname %2d];
+use Test::More tests => 2;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+my $app = [d2% appname %2d]->to_app;
+ok( is_coderef($app), 'Got app' );
+
+my $test = Plack::Test->create($app);
+my $res = $test->request( GET '/' );
+
+ok( $res->is_success, '[GET /] successful' );
diff --git a/share/skel/views/index.tt b/share/skel/views/index.tt
new file mode 100644
index 00000000..51e32a2f
--- /dev/null
+++ b/share/skel/views/index.tt
@@ -0,0 +1,148 @@
+
+<!--
+ Credit goes to the Ruby on Rails team for this page
+ has been heavily based on the default Rails page that is
+ built with a scaffolded application.
+
+ Thanks a lot to them for their work.
+
+ See Ruby on Rails if you want a kickass framework in Ruby:
+ http://www.rubyonrails.org/
+-->
+
+<div id="page">
+ <div id="sidebar">
+ <ul id="sidebar-items">
+ <li>
+ <h3>Join the community</h3>
+ <ul class="links">
+
+ <li><a href="http://perldancer.org/">PerlDancer</a></li>
+ <li><a href="http://twitter.com/PerlDancer/">Official Twitter</a></li>
+ <li><a href="https://github.com/PerlDancer/Dancer2/">GitHub Community</a></li>
+ </ul>
+ </li>
+
+ <li>
+ <h3>Browse the documentation</h3>
+
+ <ul class="links">
+ <li><a
+ href="https://metacpan.org/pod/Dancer2::Manual">Introduction</a></li>
+ <li><a href="https://metacpan.org/pod/Dancer2::Cookbook">Cookbook</a></li>
+ <li><a
+ href="https://metacpan.org/pod/Dancer2::Tutorial"
+ title="a tutorial to build a small blog engine with Dancer">Tutorial</a></li>
+ </ul>
+ </li>
+
+ <li>
+ <h3>Your application's environment</h3>
+
+ <ul>
+ <li>Location: <span class="filepath">[d2% appdir %2d]</span></li>
+ <li>Template engine: <span class="app-info"><% settings.template %></span></li>
+ <li>Logger: <span class="app-info"><% settings.logger %></span></li>
+ <li>Environment: <span class="app-info"><% settings.environment %></span></li>
+ </ul>
+
+ </li>
+ </ul>
+
+ </div>
+
+ <div id="content">
+ <div id="header">
+ <h1>Perl is dancing</h1>
+ <h2>You&rsquo;ve joined the dance floor!</h2>
+ </div>
+
+ <div id="getting-started">
+ <h1>Getting started</h1>
+ <h2>Here&rsquo;s how to get dancing:</h2>
+
+ <h3><a href="#" id="about_env_link">About your application's environment</a></h3>
+
+ <div id="about-content" style="display: none;">
+ <table>
+ <tbody>
+ <tr>
+ <td>Perl version</td>
+ <td><span class="app-info"><% perl_version %></span></td>
+ </tr>
+ <tr>
+ <td>Dancer2 version</td>
+ <td><span class="app-info"><% dancer_version %></span></td>
+ </tr>
+ <tr>
+ <td>Backend</td>
+ <td><span class="app-info"><% settings.apphandler %></span></td>
+ </tr>
+ <tr>
+ <td>Appdir</td>
+ <td><span class="filepath">[d2% appdir %2d]</span></td>
+ </tr>
+ <tr>
+ <td>Template engine</td>
+ <td><span class="app-info"><% settings.template %></span></td>
+ </tr>
+ <tr>
+ <td>Logger engine</td>
+ <td><span class="app-info"><% settings.logger %></span></td>
+ </tr>
+ <tr>
+ <td>Running environment</td>
+ <td><span class="app-info"><% settings.environment %></span></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <script type="text/javascript">
+ $('#about_env_link').click(function() {
+ $('#about-content').slideToggle('fast', function() {
+ // ok
+ });
+ return false;
+ });
+ </script>
+
+
+ <ol>
+ <li>
+ <h2>Tune your application</h2>
+
+ <p>
+ Your application is configured via a global configuration file,
+ <span class="filepath">config.yml</span> and an "environment" configuration file,
+ <span class="filepath">environments/development.yml</span>. Edit those files if you
+ want to change the settings of your application.
+ </p>
+ </li>
+
+ <li>
+ <h2>Add your own routes</h2>
+
+ <p>
+ The default route that displays this page can be removed,
+ it's just here to help you get started. The template used to
+ generate this content is located in
+ <span class="filepath">views/index.tt</span>.
+ You can add some routes to <span class="filepath">[d2% appfile %2d]</span>.
+ </p>
+ </li>
+
+ <li>
+ <h2>Enjoy web development again</h2>
+
+ <p>
+ Once you've made your changes, restart your standalone server
+ <span class="filepath">(bin/app.psgi)</span> and you're ready
+ to test your web application.
+ </p>
+ </li>
+
+ </ol>
+ </div>
+ </div>
+ </div>
diff --git a/share/skel/views/layouts/main.tt b/share/skel/views/layouts/main.tt
new file mode 100644
index 00000000..2bc29368
--- /dev/null
+++ b/share/skel/views/layouts/main.tt
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="<% settings.charset %>">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+ <title><% title %></title>
+ <link rel="stylesheet" href="<% request.uri_base %>/css/style.css">
+
+<!-- Grab jQuery from a CDN, fall back to local if necessary -->
+<script src="//code.jquery.com/jquery-3.4.1.min.js"></script>
+<script type="text/javascript">/* <![CDATA[ */
+ !window.jQuery && document.write('<script type="text/javascript" src="<% request.uri_base %>/javascripts/jquery.js"><\/script>')
+/* ]]> */</script>
+
+</head>
+<body>
+<% content %>
+<div id="footer">
+Powered by <a href="http://perldancer.org/">Dancer2</a> <% dancer_version %>
+</div>
+</body>
+</html>
diff --git a/t/00-compile.t b/t/00-compile.t
new file mode 100644
index 00000000..a0ab3db4
--- /dev/null
+++ b/t/00-compile.t
@@ -0,0 +1,153 @@
+use 5.006;
+use strict;
+use warnings;
+
+# this test was generated with Dist::Zilla::Plugin::Test::Compile 2.058
+
+use Test::More;
+
+plan tests => 58 + ($ENV{AUTHOR_TESTING} ? 1 : 0);
+
+my @module_files = (
+ 'Dancer2.pm',
+ 'Dancer2/CLI.pm',
+ 'Dancer2/CLI/Command/gen.pm',
+ 'Dancer2/CLI/Command/version.pm',
+ 'Dancer2/Core.pm',
+ 'Dancer2/Core/App.pm',
+ 'Dancer2/Core/Cookie.pm',
+ 'Dancer2/Core/DSL.pm',
+ 'Dancer2/Core/Dispatcher.pm',
+ 'Dancer2/Core/Error.pm',
+ 'Dancer2/Core/Factory.pm',
+ 'Dancer2/Core/HTTP.pm',
+ 'Dancer2/Core/Hook.pm',
+ 'Dancer2/Core/MIME.pm',
+ 'Dancer2/Core/Request.pm',
+ 'Dancer2/Core/Request/Upload.pm',
+ 'Dancer2/Core/Response.pm',
+ 'Dancer2/Core/Response/Delayed.pm',
+ 'Dancer2/Core/Role/ConfigReader.pm',
+ 'Dancer2/Core/Role/DSL.pm',
+ 'Dancer2/Core/Role/Engine.pm',
+ 'Dancer2/Core/Role/Handler.pm',
+ 'Dancer2/Core/Role/HasLocation.pm',
+ 'Dancer2/Core/Role/Hookable.pm',
+ 'Dancer2/Core/Role/Logger.pm',
+ 'Dancer2/Core/Role/Serializer.pm',
+ 'Dancer2/Core/Role/SessionFactory.pm',
+ 'Dancer2/Core/Role/SessionFactory/File.pm',
+ 'Dancer2/Core/Role/StandardResponses.pm',
+ 'Dancer2/Core/Role/Template.pm',
+ 'Dancer2/Core/Route.pm',
+ 'Dancer2/Core/Runner.pm',
+ 'Dancer2/Core/Session.pm',
+ 'Dancer2/Core/Time.pm',
+ 'Dancer2/Core/Types.pm',
+ 'Dancer2/FileUtils.pm',
+ 'Dancer2/Handler/AutoPage.pm',
+ 'Dancer2/Handler/File.pm',
+ 'Dancer2/Logger/Capture.pm',
+ 'Dancer2/Logger/Capture/Trap.pm',
+ 'Dancer2/Logger/Console.pm',
+ 'Dancer2/Logger/Diag.pm',
+ 'Dancer2/Logger/File.pm',
+ 'Dancer2/Logger/Note.pm',
+ 'Dancer2/Logger/Null.pm',
+ 'Dancer2/Plugin.pm',
+ 'Dancer2/Serializer/Dumper.pm',
+ 'Dancer2/Serializer/JSON.pm',
+ 'Dancer2/Serializer/Mutable.pm',
+ 'Dancer2/Serializer/YAML.pm',
+ 'Dancer2/Session/Simple.pm',
+ 'Dancer2/Session/YAML.pm',
+ 'Dancer2/Template/Implementation/ForkedTiny.pm',
+ 'Dancer2/Template/Simple.pm',
+ 'Dancer2/Template/TemplateToolkit.pm',
+ 'Dancer2/Template/Tiny.pm',
+ 'Dancer2/Test.pm'
+);
+
+my @scripts = (
+ 'script/dancer2'
+);
+
+# no fake home requested
+
+my @switches = (
+ -d 'blib' ? '-Mblib' : '-Ilib',
+);
+
+use File::Spec;
+use IPC::Open3;
+use IO::Handle;
+
+open my $stdin, '<', File::Spec->devnull or die "can't open devnull: $!";
+
+my @warnings;
+for my $lib (@module_files)
+{
+ # see L<perlfaq8/How can I capture STDERR from an external command?>
+ my $stderr = IO::Handle->new;
+
+ diag('Running: ', join(', ', map { my $str = $_; $str =~ s/'/\\'/g; q{'} . $str . q{'} }
+ $^X, @switches, '-e', "require q[$lib]"))
+ if $ENV{PERL_COMPILE_TEST_DEBUG};
+
+ my $pid = open3($stdin, '>&STDERR', $stderr, $^X, @switches, '-e', "require q[$lib]");
+ binmode $stderr, ':crlf' if $^O eq 'MSWin32';
+ my @_warnings = <$stderr>;
+ waitpid($pid, 0);
+ is($?, 0, "$lib loaded ok");
+
+ shift @_warnings if @_warnings and $_warnings[0] =~ /^Using .*\bblib/
+ and not eval { +require blib; blib->VERSION('1.01') };
+
+ if (@_warnings)
+ {
+ warn @_warnings;
+ push @warnings, @_warnings;
+ }
+}
+
+foreach my $file (@scripts)
+{ SKIP: {
+ open my $fh, '<', $file or warn("Unable to open $file: $!"), next;
+ my $line = <$fh>;
+
+ close $fh and skip("$file isn't perl", 1) unless $line =~ /^#!\s*(?:\S*perl\S*)((?:\s+-\w*)*)(?:\s*#.*)?$/;
+ @switches = (@switches, split(' ', $1)) if $1;
+
+ 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};
+
+
diff --git a/t/00-report-prereqs.dd b/t/00-report-prereqs.dd
new file mode 100644
index 00000000..70379f51
--- /dev/null
+++ b/t/00-report-prereqs.dd
@@ -0,0 +1,127 @@
+do { my $x = {
+ 'build' => {
+ 'requires' => {
+ 'ExtUtils::MakeMaker' => '7.1101',
+ 'Test::CPAN::Meta' => '0'
+ }
+ },
+ 'configure' => {
+ 'requires' => {
+ 'CPAN::Meta::Requirements' => '2.120620',
+ 'ExtUtils::MakeMaker' => '7.1101',
+ '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' => {
+ 'YAML' => '1.16'
+ },
+ 'recommends' => {
+ 'CGI::Deurl::XS' => '0',
+ 'Class::XSAccessor' => '0',
+ 'Cpanel::JSON::XS' => '0',
+ 'Crypt::URandom' => '0',
+ 'HTTP::XSCookies' => '0.000007',
+ 'HTTP::XSHeaders' => '0',
+ 'Math::Random::ISAAC::XS' => '0',
+ 'MooX::TypeTiny' => '0',
+ 'Pod::Simple::Search' => '0',
+ 'Pod::Simple::SimpleTree' => '0',
+ 'Scope::Upper' => '0',
+ 'Type::Tiny::XS' => '0',
+ 'URL::Encode::XS' => '0',
+ 'YAML::XS' => '0'
+ },
+ 'requires' => {
+ 'App::Cmd::Setup' => '0',
+ 'Attribute::Handlers' => '0',
+ 'Carp' => '0',
+ 'Clone' => '0',
+ 'Config::Any' => '0',
+ 'Digest::SHA' => '0',
+ 'Encode' => '0',
+ 'Exporter' => '5.57',
+ 'Exporter::Tiny' => '0',
+ 'File::Basename' => '0',
+ 'File::Copy' => '0',
+ 'File::Find' => '0',
+ 'File::Path' => '0',
+ 'File::Share' => '0',
+ 'File::Spec' => '0',
+ 'File::Temp' => '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::Runtime' => '0',
+ 'Moo' => '2.000000',
+ 'Moo::Role' => '0',
+ 'POSIX' => '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',
+ 'Template' => '0',
+ 'Template::Tiny' => '0',
+ 'Test::Builder' => '0',
+ 'Test::More' => '0.92',
+ 'Type::Tiny' => '1.000006',
+ 'Types::Standard' => '0',
+ 'URI::Escape' => '0',
+ 'YAML' => '0.86',
+ 'parent' => '0'
+ },
+ 'suggests' => {
+ 'Fcntl' => '0',
+ 'MIME::Types' => '0'
+ }
+ },
+ 'test' => {
+ 'recommends' => {
+ 'CPAN::Meta' => '2.120900'
+ },
+ 'requires' => {
+ 'Capture::Tiny' => '0.12',
+ 'ExtUtils::MakeMaker' => '7.1101',
+ 'File::Spec' => '0',
+ 'HTTP::Cookies' => '0',
+ 'HTTP::Headers' => '0',
+ 'IO::Handle' => '0',
+ 'IPC::Open3' => '0',
+ 'Template' => '0',
+ 'Test::Builder' => '0',
+ 'Test::EOL' => '0',
+ 'Test::Fatal' => '0',
+ 'Test::More' => '0.92'
+ }
+ }
+ };
+ $x;
+ } \ No newline at end of file
diff --git a/t/00-report-prereqs.t b/t/00-report-prereqs.t
new file mode 100644
index 00000000..c72183a1
--- /dev/null
+++ b/t/00-report-prereqs.t
@@ -0,0 +1,193 @@
+#!perl
+
+use strict;
+use warnings;
+
+# This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs 0.027
+
+use Test::More tests => 1;
+
+use ExtUtils::MakeMaker;
+use File::Spec;
+
+# from $version::LAX
+my $lax_version_re =
+ qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )?
+ |
+ (?:\.[0-9]+) (?:_[0-9]+)?
+ ) | (?:
+ v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )?
+ |
+ (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)?
+ )
+ )/x;
+
+# hide optional CPAN::Meta modules from prereq scanner
+# and check if they are available
+my $cpan_meta = "CPAN::Meta";
+my $cpan_meta_pre = "CPAN::Meta::Prereqs";
+my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic
+
+# Verify requirements?
+my $DO_VERIFY_PREREQS = 1;
+
+sub _max {
+ my $max = shift;
+ $max = ( $_ > $max ) ? $_ : $max for @_;
+ return $max;
+}
+
+sub _merge_prereqs {
+ my ($collector, $prereqs) = @_;
+
+ # CPAN::Meta::Prereqs object
+ if (ref $collector eq $cpan_meta_pre) {
+ return $collector->with_merged_prereqs(
+ CPAN::Meta::Prereqs->new( $prereqs )
+ );
+ }
+
+ # Raw hashrefs
+ for my $phase ( keys %$prereqs ) {
+ for my $type ( keys %{ $prereqs->{$phase} } ) {
+ for my $module ( keys %{ $prereqs->{$phase}{$type} } ) {
+ $collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module};
+ }
+ }
+ }
+
+ return $collector;
+}
+
+my @include = qw(
+
+);
+
+my @exclude = qw(
+
+);
+
+# Add static prereqs to the included modules list
+my $static_prereqs = do './t/00-report-prereqs.dd';
+
+# Merge all prereqs (either with ::Prereqs or a hashref)
+my $full_prereqs = _merge_prereqs(
+ ( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ),
+ $static_prereqs
+);
+
+# Add dynamic prereqs to the included modules list (if we can)
+my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
+my $cpan_meta_error;
+if ( $source && $HAS_CPAN_META
+ && (my $meta = eval { CPAN::Meta->load_file($source) } )
+) {
+ $full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs);
+}
+else {
+ $cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source)
+ $source = 'static metadata';
+}
+
+my @full_reports;
+my @dep_errors;
+my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs;
+
+# Add static includes into a fake section
+for my $mod (@include) {
+ $req_hash->{other}{modules}{$mod} = 0;
+}
+
+for my $phase ( qw(configure build test runtime develop other) ) {
+ next unless $req_hash->{$phase};
+ next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING});
+
+ for my $type ( qw(requires recommends suggests conflicts modules) ) {
+ next unless $req_hash->{$phase}{$type};
+
+ my $title = ucfirst($phase).' '.ucfirst($type);
+ my @reports = [qw/Module Want Have/];
+
+ for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) {
+ next if $mod eq 'perl';
+ next if grep { $_ eq $mod } @exclude;
+
+ my $file = $mod;
+ $file =~ s{::}{/}g;
+ $file .= ".pm";
+ my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC;
+
+ my $want = $req_hash->{$phase}{$type}{$mod};
+ $want = "undef" unless defined $want;
+ $want = "any" if !$want && $want == 0;
+
+ my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required";
+
+ if ($prefix) {
+ my $have = MM->parse_version( File::Spec->catfile($prefix, $file) );
+ $have = "undef" unless defined $have;
+ push @reports, [$mod, $want, $have];
+
+ if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) {
+ if ( $have !~ /\A$lax_version_re\z/ ) {
+ push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)";
+ }
+ elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) {
+ push @dep_errors, "$mod version '$have' is not in required range '$want'";
+ }
+ }
+ }
+ else {
+ push @reports, [$mod, $want, "missing"];
+
+ if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) {
+ push @dep_errors, "$mod is not installed ($req_string)";
+ }
+ }
+ }
+
+ if ( @reports ) {
+ push @full_reports, "=== $title ===\n\n";
+
+ my $ml = _max( map { length $_->[0] } @reports );
+ my $wl = _max( map { length $_->[1] } @reports );
+ my $hl = _max( map { length $_->[2] } @reports );
+
+ if ($type eq 'modules') {
+ splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl];
+ push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports;
+ }
+ else {
+ splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl];
+ push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports;
+ }
+
+ push @full_reports, "\n";
+ }
+ }
+}
+
+if ( @full_reports ) {
+ diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports;
+}
+
+if ( $cpan_meta_error || @dep_errors ) {
+ diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n";
+}
+
+if ( $cpan_meta_error ) {
+ my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml';
+ diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n";
+}
+
+if ( @dep_errors ) {
+ diag join("\n",
+ "\nThe following REQUIRED prerequisites were not satisfied:\n",
+ @dep_errors,
+ "\n"
+ );
+}
+
+pass;
+
+# vim: ts=4 sts=4 sw=4 et:
diff --git a/t/app.t b/t/app.t
new file mode 100644
index 00000000..1081d2a9
--- /dev/null
+++ b/t/app.t
@@ -0,0 +1,216 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Fatal;
+use Dancer2;
+use Dancer2::Core::App;
+use Dancer2::Core::Dispatcher;
+use Dancer2::Core::Hook;
+use Dancer2::FileUtils;
+use File::Spec;
+
+# our app/dispatcher object
+my $app = Dancer2::Core::App->new( name => 'main', );
+$app->setting( show_errors => 1 ); # enable show errors
+my $dispatcher = Dancer2::Core::Dispatcher->new( apps => [$app] );
+
+# first basic tests
+isa_ok $app, 'Dancer2::Core::App';
+
+# some routes to play with
+my @routes = (
+ { method => 'get',
+ regexp => '/',
+ code => sub {'/'},
+ },
+ { method => 'get',
+ regexp => '/blog',
+ code => sub {'/blog'},
+ },
+);
+
+# testing with and without prefixes
+for my $p ( '/', '/mywebsite' ) {
+ for my $r (@routes) {
+ $app->prefix($p);
+ $app->add_route(%$r);
+ }
+}
+
+is $app->environment, 'development';
+
+my $routes_regexps = $app->routes_regexps_for('get');
+is( scalar(@$routes_regexps), 4, "route regexps are OK" );
+
+for my $path ( '/', '/blog', '/mywebsite/', '/mywebsite/blog', ) {
+ my $env = {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => $path
+ };
+
+ my $expected = {
+ '/' => '/',
+ '/blog' => '/blog',
+ '/mywebsite/' => '/',
+ '/mywebsite/blog' => '/blog',
+ };
+
+ my $resp = $dispatcher->dispatch($env);
+ is $resp->[0], 200, 'got a 200';
+ is $resp->[2][0], $expected->{$path}, 'got expected route';
+}
+
+note "testing lexical prefixes";
+
+# clear the prefix in $app (and by the way, makes sure it works when prefix is
+# undef).
+$app->prefix(undef);
+
+# nested prefixes bitches!
+$app->lexical_prefix(
+ '/foo' => sub {
+ $app->add_route(
+ method => 'get',
+ regexp => '/',
+ code => sub {'/foo/'}
+ );
+
+ $app->add_route(
+ method => 'get',
+ regexp => '/second',
+ code => sub {'/foo/second'}
+ );
+
+ $app->lexical_prefix(
+ '/bar' => sub {
+ $app->add_route(
+ method => 'get',
+ regexp => '/',
+ code => sub {'/foo/bar'}
+ );
+ $app->add_route(
+ method => 'get',
+ regexp => '/second',
+ code => sub {'/foo/bar/second'}
+ );
+ }
+ );
+ },
+);
+
+# to make sure the lexical prefix did not crash anything
+$app->add_route(
+ method => 'get',
+ regexp => '/root',
+ code => sub {'/root'}
+);
+
+# make sure a meaningless lexical prefix is ignored
+$app->lexical_prefix(
+ '/' => sub {
+ $app->add_route(
+ method => 'get',
+ regexp => '/somewhere',
+ code => sub {'/somewhere'},
+ );
+ }
+);
+
+for
+ my $path ( '/foo/', '/foo/second', '/foo/bar/second', '/root', '/somewhere' )
+{
+ my $env = {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => $path,
+ };
+
+ my $resp = $dispatcher->dispatch($env);
+ is $resp->[0], 200, 'got a 200';
+ is $resp->[2][0], $path, 'got expected route';
+}
+
+note "test a failure in the callback of a lexical prefix";
+like(
+ exception {
+ $app->lexical_prefix( '/test' => sub { Failure->game_over() } );
+ },
+ qr{Unable to run the callback for prefix '/test': Can't locate object method "game_over" via package "Failure"},
+ "caught an exception in the lexical prefix callback",
+);
+
+$app->add_hook(
+ Dancer2::Core::Hook->new(
+ name => 'before',
+ code => sub {1},
+ )
+);
+
+$app->add_hook(
+ Dancer2::Core::Hook->new(
+ name => 'before',
+ code => sub { Foo->failure; },
+ )
+);
+
+$app->compile_hooks;
+my $env = {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => '/',
+};
+
+like(
+ $dispatcher->dispatch($env)->[2][0],
+ qr/Exception caught in &#39;core.app.before_request&#39; filter: Hook error: Can&#39;t locate object method &quot;failure&quot;/,
+ 'before filter nonexistent method failure',
+);
+
+$app->replace_hook( 'core.app.before_request', [ sub {1} ] );
+$app->compile_hooks;
+$env = {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => '/',
+};
+
+# test duplicate routes when the path is a regex
+$app = Dancer2::Core::App->new( name => 'main' );
+my $regexp_route = {
+ method => 'get', 'regexp' => qr!/(\d+)!, code => sub {1}
+};
+$app->add_route(%$regexp_route);
+
+# try to get an invalid engine
+eval {$app->engine('foo')};
+like(
+ $@,
+ qr/^Engine 'foo' is not supported/,
+ "Engine 'foo' does not exist",
+);
+
+my $tmpl_engine = $app->engine('template');
+ok $tmpl_engine, "Template engine is defined";
+
+ok !$app->has_serializer_engine, "Serializer engine does not exist";
+
+is_deeply(
+ $app->_get_config_for_engine('NonExistent'),
+ {},
+ 'Empty configuration for nonexistent engine',
+);
+
+# TODO: not such an intelligent check, this one...
+# set configuration for an engine
+$app->config->{'engines'}{'template'}{'Tiny'}{'hello'} = 'world';
+
+is_deeply(
+ $app->_get_config_for_engine( template => 'Tiny', $app->config ),
+ { hello => 'world' },
+ '_get_config_for_engine can find the right configuration',
+);
+
+is(
+ File::Spec->canonpath( $app->caller ),
+ File::Spec->catfile(t => 'app.t'),
+ 'Correct caller for app',
+);
+
+done_testing;
diff --git a/t/app/t1/bin/app.psgi b/t/app/t1/bin/app.psgi
new file mode 100644
index 00000000..ef833528
--- /dev/null
+++ b/t/app/t1/bin/app.psgi
@@ -0,0 +1,6 @@
+#!perl
+
+use Dancer2;
+use App1;
+
+start;
diff --git a/t/app/t1/config.yml b/t/app/t1/config.yml
new file mode 100644
index 00000000..b9fc7732
--- /dev/null
+++ b/t/app/t1/config.yml
@@ -0,0 +1,2 @@
+app:
+ config: ok
diff --git a/t/app/t1/lib/App1.pm b/t/app/t1/lib/App1.pm
new file mode 100644
index 00000000..62a86f3c
--- /dev/null
+++ b/t/app/t1/lib/App1.pm
@@ -0,0 +1,6 @@
+package App1;
+use strict;
+use warnings;
+use Dancer2;
+
+1;
diff --git a/t/app/t1/lib/Sub/App2.pm b/t/app/t1/lib/Sub/App2.pm
new file mode 100644
index 00000000..908b632f
--- /dev/null
+++ b/t/app/t1/lib/Sub/App2.pm
@@ -0,0 +1,6 @@
+package Sub::App2;
+use strict;
+use warnings;
+use Dancer2;
+
+1;
diff --git a/t/app/t2/.dancer b/t/app/t2/.dancer
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/t/app/t2/.dancer
@@ -0,0 +1 @@
+
diff --git a/t/app/t2/config.yml b/t/app/t2/config.yml
new file mode 100644
index 00000000..b9fc7732
--- /dev/null
+++ b/t/app/t2/config.yml
@@ -0,0 +1,2 @@
+app:
+ config: ok
diff --git a/t/app/t2/lib/App3.pm b/t/app/t2/lib/App3.pm
new file mode 100644
index 00000000..7680b204
--- /dev/null
+++ b/t/app/t2/lib/App3.pm
@@ -0,0 +1,5 @@
+package App3;
+use strict;
+use warnings;
+use Dancer2;
+1;
diff --git a/t/app_alone.t b/t/app_alone.t
new file mode 100644
index 00000000..5fa05f06
--- /dev/null
+++ b/t/app_alone.t
@@ -0,0 +1,25 @@
+#!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' );
+};
+
diff --git a/t/author-no-tabs.t b/t/author-no-tabs.t
new file mode 100644
index 00000000..9ed89298
--- /dev/null
+++ b/t/author-no-tabs.t
@@ -0,0 +1,366 @@
+
+BEGIN {
+ unless ($ENV{AUTHOR_TESTING}) {
+ print qq{1..0 # SKIP these tests are for testing by the author\n};
+ exit
+ }
+}
+
+use strict;
+use warnings;
+
+# this test was generated with Dist::Zilla::Plugin::Test::NoTabs 0.15
+
+use Test::More 0.88;
+use Test::NoTabs;
+
+my @files = (
+ 'lib/Dancer2.pm',
+ 'lib/Dancer2/CLI.pm',
+ 'lib/Dancer2/CLI/Command/gen.pm',
+ 'lib/Dancer2/CLI/Command/version.pm',
+ 'lib/Dancer2/Config.pod',
+ 'lib/Dancer2/Cookbook.pod',
+ 'lib/Dancer2/Core.pm',
+ 'lib/Dancer2/Core/App.pm',
+ 'lib/Dancer2/Core/Cookie.pm',
+ 'lib/Dancer2/Core/DSL.pm',
+ 'lib/Dancer2/Core/Dispatcher.pm',
+ 'lib/Dancer2/Core/Error.pm',
+ 'lib/Dancer2/Core/Factory.pm',
+ 'lib/Dancer2/Core/HTTP.pm',
+ 'lib/Dancer2/Core/Hook.pm',
+ 'lib/Dancer2/Core/MIME.pm',
+ 'lib/Dancer2/Core/Request.pm',
+ 'lib/Dancer2/Core/Request/Upload.pm',
+ 'lib/Dancer2/Core/Response.pm',
+ 'lib/Dancer2/Core/Response/Delayed.pm',
+ 'lib/Dancer2/Core/Role/ConfigReader.pm',
+ 'lib/Dancer2/Core/Role/DSL.pm',
+ 'lib/Dancer2/Core/Role/Engine.pm',
+ 'lib/Dancer2/Core/Role/Handler.pm',
+ 'lib/Dancer2/Core/Role/HasLocation.pm',
+ 'lib/Dancer2/Core/Role/Hookable.pm',
+ 'lib/Dancer2/Core/Role/Logger.pm',
+ 'lib/Dancer2/Core/Role/Serializer.pm',
+ 'lib/Dancer2/Core/Role/SessionFactory.pm',
+ 'lib/Dancer2/Core/Role/SessionFactory/File.pm',
+ 'lib/Dancer2/Core/Role/StandardResponses.pm',
+ 'lib/Dancer2/Core/Role/Template.pm',
+ 'lib/Dancer2/Core/Route.pm',
+ 'lib/Dancer2/Core/Runner.pm',
+ 'lib/Dancer2/Core/Session.pm',
+ 'lib/Dancer2/Core/Time.pm',
+ 'lib/Dancer2/Core/Types.pm',
+ 'lib/Dancer2/FileUtils.pm',
+ 'lib/Dancer2/Handler/AutoPage.pm',
+ 'lib/Dancer2/Handler/File.pm',
+ 'lib/Dancer2/Logger/Capture.pm',
+ 'lib/Dancer2/Logger/Capture/Trap.pm',
+ 'lib/Dancer2/Logger/Console.pm',
+ 'lib/Dancer2/Logger/Diag.pm',
+ 'lib/Dancer2/Logger/File.pm',
+ 'lib/Dancer2/Logger/Note.pm',
+ 'lib/Dancer2/Logger/Null.pm',
+ 'lib/Dancer2/Manual.pod',
+ 'lib/Dancer2/Manual/Deployment.pod',
+ 'lib/Dancer2/Manual/Migration.pod',
+ 'lib/Dancer2/Manual/Testing.pod',
+ 'lib/Dancer2/Plugin.pm',
+ 'lib/Dancer2/Plugins.pod',
+ 'lib/Dancer2/Policy.pod',
+ 'lib/Dancer2/Serializer/Dumper.pm',
+ 'lib/Dancer2/Serializer/JSON.pm',
+ 'lib/Dancer2/Serializer/Mutable.pm',
+ 'lib/Dancer2/Serializer/YAML.pm',
+ 'lib/Dancer2/Session/Simple.pm',
+ 'lib/Dancer2/Session/YAML.pm',
+ 'lib/Dancer2/Template/Implementation/ForkedTiny.pm',
+ 'lib/Dancer2/Template/Simple.pm',
+ 'lib/Dancer2/Template/TemplateToolkit.pm',
+ 'lib/Dancer2/Template/Tiny.pm',
+ 'lib/Dancer2/Test.pm',
+ 'lib/Dancer2/Tutorial.pod',
+ 'script/dancer2',
+ 't/00-compile.t',
+ 't/00-report-prereqs.dd',
+ 't/00-report-prereqs.t',
+ 't/app.t',
+ 't/app/t1/bin/app.psgi',
+ 't/app/t1/config.yml',
+ 't/app/t1/lib/App1.pm',
+ 't/app/t1/lib/Sub/App2.pm',
+ 't/app/t2/.dancer',
+ 't/app/t2/config.yml',
+ 't/app/t2/lib/App3.pm',
+ 't/app_alone.t',
+ 't/auto_page.t',
+ 't/caller.t',
+ 't/charset_server.t',
+ 't/classes/Dancer2-Core-Factory/new.t',
+ 't/classes/Dancer2-Core-Hook/new.t',
+ 't/classes/Dancer2-Core-Request/new.t',
+ 't/classes/Dancer2-Core-Request/serializers.t',
+ 't/classes/Dancer2-Core-Response-Delayed/after_hooks.t',
+ 't/classes/Dancer2-Core-Response-Delayed/new.t',
+ 't/classes/Dancer2-Core-Response/new_from.t',
+ 't/classes/Dancer2-Core-Role-Engine/with.t',
+ 't/classes/Dancer2-Core-Role-Handler/with.t',
+ 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists',
+ 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists',
+ 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer',
+ 't/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl',
+ 't/classes/Dancer2-Core-Role-HasLocation/with.t',
+ 't/classes/Dancer2-Core-Role-Serializer/with.t',
+ 't/classes/Dancer2-Core-Role-StandardResponses/with.t',
+ 't/classes/Dancer2-Core-Route/base.t',
+ 't/classes/Dancer2-Core-Route/deprecated_param_keys.t',
+ 't/classes/Dancer2-Core-Route/match.t',
+ 't/classes/Dancer2-Core-Runner/environment.t',
+ 't/classes/Dancer2-Core-Runner/new.t',
+ 't/classes/Dancer2-Core-Runner/psgi_app.t',
+ 't/classes/Dancer2-Core/camelize.t',
+ 't/classes/Dancer2/import-pragmas.t',
+ 't/classes/Dancer2/import.t',
+ 't/config.yml',
+ 't/config/config.yml',
+ 't/config/environments/failure.yml',
+ 't/config/environments/merging.yml',
+ 't/config/environments/production.yml',
+ 't/config/environments/staging.json',
+ 't/config2/config.yml',
+ 't/config2/config_local.yml',
+ 't/config2/environments/lconfig.yml',
+ 't/config2/environments/lconfig_local.yml',
+ 't/config_multiapp.t',
+ 't/config_reader.t',
+ 't/config_settings.t',
+ 't/context-in-before.t',
+ 't/cookie.t',
+ 't/corpus/pretty/505.tt',
+ 't/corpus/pretty/relative.tt',
+ 't/corpus/pretty_public/404.html',
+ 't/corpus/pretty_public/510.html',
+ 't/corpus/static/index.html',
+ 't/custom_dsl.t',
+ 't/dancer-test.t',
+ 't/dancer-test/config.yml',
+ 't/deserialize.t',
+ 't/disp_named_capture.t',
+ 't/dispatcher.t',
+ 't/dsl/any.t',
+ 't/dsl/app.t',
+ 't/dsl/content.t',
+ 't/dsl/delayed.t',
+ 't/dsl/error_template.t',
+ 't/dsl/extend.t',
+ 't/dsl/extend_config/config.yml',
+ 't/dsl/halt.t',
+ 't/dsl/halt_with_param.t',
+ 't/dsl/json.t',
+ 't/dsl/parameters.t',
+ 't/dsl/pass.t',
+ 't/dsl/path.t',
+ 't/dsl/request.t',
+ 't/dsl/route_retvals.t',
+ 't/dsl/send_as.t',
+ 't/dsl/send_file.t',
+ 't/dsl/splat.t',
+ 't/dsl/to_app.t',
+ 't/engine.t',
+ 't/error.t',
+ 't/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-596.t',
+ 't/issues/gh-634.t',
+ 't/issues/gh-639/fails/.dancer',
+ 't/issues/gh-639/fails/config.yml',
+ 't/issues/gh-639/fails/issue.t',
+ 't/issues/gh-639/succeeds/.dancer',
+ 't/issues/gh-639/succeeds/config.yml',
+ 't/issues/gh-639/succeeds/issue.t',
+ 't/issues/gh-650/gh-650.t',
+ 't/issues/gh-650/views/environment_setting.tt',
+ 't/issues/gh-723.t',
+ 't/issues/gh-730.t',
+ 't/issues/gh-762.t',
+ 't/issues/gh-762/views/404.tt',
+ 't/issues/gh-794.t',
+ 't/issues/gh-797.t',
+ 't/issues/gh-799.t',
+ 't/issues/gh-811.t',
+ 't/issues/gh-931.t',
+ 't/issues/gh-936.t',
+ 't/issues/gh-936/views/error.tt',
+ 't/issues/gh-944.t',
+ 't/issues/gh-975/config.yml',
+ 't/issues/gh-975/gh-975.t',
+ 't/issues/gh-975/test_public_dir/test.txt',
+ 't/issues/memleak/die_in_hooks.t',
+ 't/issues/vars-in-forward.t',
+ 't/lib/App1.pm',
+ 't/lib/App2.pm',
+ 't/lib/Dancer2/Plugin/Bar.pm',
+ 't/lib/Dancer2/Plugin/DancerPlugin.pm',
+ 't/lib/Dancer2/Plugin/DefineKeywords.pm',
+ 't/lib/Dancer2/Plugin/EmptyPlugin.pm',
+ 't/lib/Dancer2/Plugin/Foo.pm',
+ 't/lib/Dancer2/Plugin/FooPlugin.pm',
+ 't/lib/Dancer2/Plugin/Hookee.pm',
+ 't/lib/Dancer2/Plugin/OnPluginImport.pm',
+ 't/lib/Dancer2/Plugin/PluginWithImport.pm',
+ 't/lib/Dancer2/Plugin/Polite.pm',
+ 't/lib/Dancer2/Session/SimpleNoChangeId.pm',
+ 't/lib/Foo.pm',
+ 't/lib/MyDancerDSL.pm',
+ 't/lib/PoC/Plugin/Polite.pm',
+ 't/lib/SubApp1.pm',
+ 't/lib/SubApp2.pm',
+ 't/lib/TestApp.pm',
+ 't/lib/TestPod.pm',
+ 't/lib/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/named_apps.t',
+ 't/no_default_middleware.t',
+ 't/plugin2/basic-2.t',
+ 't/plugin2/basic.t',
+ 't/plugin2/define-keywords.t',
+ 't/plugin2/find_plugin.t',
+ 't/plugin2/from-config.t',
+ 't/plugin2/hooks.t',
+ 't/plugin2/inside-plugin.t',
+ 't/plugin2/keywords-hooks-namespace.t',
+ 't/plugin2/memory_cycles.t',
+ 't/plugin2/no-app-munging.t',
+ 't/plugin2/no-clobbering.t',
+ 't/plugin2/no-config.t',
+ 't/plugin2/with-plugins.t',
+ 't/plugin_import.t',
+ 't/plugin_multiple_apps.t',
+ 't/plugin_register.t',
+ 't/plugin_syntax.t',
+ 't/prepare_app.t',
+ 't/psgi_app.t',
+ 't/psgi_app_forward_and_pass.t',
+ 't/public/file.txt',
+ 't/redirect.t',
+ 't/request.t',
+ 't/request_make_forward_to.t',
+ 't/request_upload.t',
+ 't/response.t',
+ 't/roles/hook.t',
+ 't/route-pod-coverage/route-pod-coverage.t',
+ 't/scope_problems/config.yml',
+ 't/scope_problems/dispatcher_internal_request.t',
+ 't/scope_problems/keywords_before_template_hook.t',
+ 't/scope_problems/session_is_cleared.t',
+ 't/scope_problems/views/500.tt',
+ 't/scope_problems/with_return_dies.t',
+ 't/serializer.t',
+ 't/serializer_json.t',
+ 't/serializer_mutable.t',
+ 't/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/shared_engines.t',
+ 't/static_content.t',
+ 't/template.t',
+ 't/template_default_tokens.t',
+ 't/template_ext.t',
+ 't/template_name.t',
+ 't/template_simple.t',
+ 't/template_tiny/01_compile.t',
+ 't/template_tiny/02_trivial.t',
+ 't/template_tiny/03_samples.t',
+ 't/template_tiny/04_compat.t',
+ 't/template_tiny/05_preparse.t',
+ 't/template_tiny/samples/01_hello.tt',
+ 't/template_tiny/samples/01_hello.txt',
+ 't/template_tiny/samples/01_hello.var',
+ 't/template_tiny/samples/02_null.tt',
+ 't/template_tiny/samples/02_null.txt',
+ 't/template_tiny/samples/02_null.var',
+ 't/template_tiny/samples/03_chomp.tt',
+ 't/template_tiny/samples/03_chomp.txt',
+ 't/template_tiny/samples/03_chomp.var',
+ 't/template_tiny/samples/04_nested.tt',
+ 't/template_tiny/samples/04_nested.txt',
+ 't/template_tiny/samples/04_nested.var',
+ 't/template_tiny/samples/05_condition.tt',
+ 't/template_tiny/samples/05_condition.txt',
+ 't/template_tiny/samples/05_condition.var',
+ 't/template_tiny/samples/06_object.tt',
+ 't/template_tiny/samples/06_object.txt',
+ 't/template_tiny/samples/06_object.var',
+ 't/template_tiny/samples/07_nesting.tt',
+ 't/template_tiny/samples/07_nesting.txt',
+ 't/template_tiny/samples/07_nesting.var',
+ 't/template_tiny/samples/08_foreach.tt',
+ 't/template_tiny/samples/08_foreach.txt',
+ 't/template_tiny/samples/08_foreach.var',
+ 't/template_tiny/samples/09_trim.tt',
+ 't/template_tiny/samples/09_trim.txt',
+ 't/template_tiny/samples/09_trim.var',
+ 't/time.t',
+ 't/types.t',
+ 't/uri_for.t',
+ 't/vars.t',
+ 't/views/auto_page.tt',
+ 't/views/beforetemplate.tt',
+ 't/views/folder/page.tt',
+ 't/views/index.tt',
+ 't/views/layouts/main.tt',
+ 't/views/session_in_template.tt',
+ 't/views/template_simple_index.tt',
+ 't/views/tokens.tt'
+);
+
+notabs_ok($_) foreach @files;
+done_testing;
diff --git a/t/author-pod-syntax.t b/t/author-pod-syntax.t
new file mode 100644
index 00000000..2233af08
--- /dev/null
+++ b/t/author-pod-syntax.t
@@ -0,0 +1,15 @@
+#!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();
diff --git a/t/auto_page.t b/t/auto_page.t
new file mode 100644
index 00000000..d1f5555e
--- /dev/null
+++ b/t/auto_page.t
@@ -0,0 +1,99 @@
+use 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;
diff --git a/t/caller.t b/t/caller.t
new file mode 100644
index 00000000..e236e656
--- /dev/null
+++ b/t/caller.t
@@ -0,0 +1,30 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 2;
+use Plack::Test;
+use HTTP::Request::Common;
+use File::Spec;
+
+{
+ package App;
+ use Dancer2;
+
+ get '/' => sub { app->caller };
+
+}
+
+my $app = App->to_app;
+test_psgi $app, sub {
+ my $cb = shift;
+ my $res = $cb->( GET '/' );
+
+ is( $res->code, 200, '[GET /] Successful' );
+ is(
+ File::Spec->canonpath( $res->content),
+ File::Spec->catfile(t => 'caller.t'),
+ 'Correct App name from caller',
+ );
+};
diff --git a/t/charset_server.t b/t/charset_server.t
new file mode 100644
index 00000000..71be764c
--- /dev/null
+++ b/t/charset_server.t
@@ -0,0 +1,58 @@
+use Test::More;
+use strict;
+use warnings;
+use Encode;
+use utf8;
+
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+{
+ 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();
diff --git a/t/classes/Dancer2-Core-Factory/new.t b/t/classes/Dancer2-Core-Factory/new.t
new file mode 100644
index 00000000..3450da3e
--- /dev/null
+++ b/t/classes/Dancer2-Core-Factory/new.t
@@ -0,0 +1,16 @@
+use strict;
+use warnings;
+use Test::More tests => 5;
+
+use_ok('Dancer2::Core::Factory');
+
+my $factory = Dancer2::Core::Factory->new;
+isa_ok( $factory, 'Dancer2::Core::Factory' );
+can_ok( $factory, 'create' );
+
+my $template = Dancer2::Core::Factory->create(
+ 'template', 'template_toolkit', layout => 'mylayout'
+);
+
+isa_ok( $template, 'Dancer2::Template::TemplateToolkit' );
+is( $template->{'layout'}, 'mylayout', 'Correct layout set in the template' );
diff --git a/t/classes/Dancer2-Core-Hook/new.t b/t/classes/Dancer2-Core-Hook/new.t
new file mode 100644
index 00000000..95b2470d
--- /dev/null
+++ b/t/classes/Dancer2-Core-Hook/new.t
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+use Test::More tests => 12;
+use Test::Fatal;
+
+use_ok('Dancer2::Core::Hook');
+
+like(
+ exception { Dancer2::Core::Hook->new( name => 'myname' ) },
+ qr/^Missing required arguments: code/,
+ 'Must provide code attribute',
+);
+
+like(
+ exception { Dancer2::Core::Hook->new( code => sub {1} ) },
+ qr/^Missing required arguments: name/,
+ 'Must provide name attribute',
+);
+
+is(
+ exception {
+ Dancer2::Core::Hook->new( name => 'myname', code => sub {1} )
+ },
+ undef,
+ 'Can create hook with name and code',
+);
+
+{
+ my $hook = Dancer2::Core::Hook->new(
+ name => 'before_template',
+ code => sub {
+ my $input = shift;
+ ::is( $input, 'input', 'Correct input for hook' );
+ return 'output';
+ },
+ );
+
+ isa_ok( $hook, 'Dancer2::Core::Hook' );
+ can_ok( $hook, qw<name code> );
+
+ is(
+ $hook->name,
+ 'before_template_render',
+ 'before_template becomes before_template_render',
+ );
+
+ isa_ok( $hook->code, 'CODE' );
+
+ is(
+ $hook->code->('input'),
+ 'output',
+ 'Hook returned proper output',
+ );
+}
+
+{
+ my $hook = Dancer2::Core::Hook->new(
+ name => 'CrashingHook',
+ code => sub { die 'dying' },
+ );
+
+ isa_ok( $hook, 'Dancer2::Core::Hook' );
+
+ like(
+ exception { $hook->code->() },
+ qr/^Hook error: dying/,
+ 'Hook crashing caught',
+ );
+}
diff --git a/t/classes/Dancer2-Core-Request/new.t b/t/classes/Dancer2-Core-Request/new.t
new file mode 100644
index 00000000..a00796d1
--- /dev/null
+++ b/t/classes/Dancer2-Core-Request/new.t
@@ -0,0 +1,521 @@
+use strict;
+use warnings;
+use Test::More tests => 10;
+use Test::Fatal;
+use Plack::Test;
+use HTTP::Request::Common;
+use Plack::Builder;
+use URI::Escape;
+
+BEGIN { use_ok('Dancer2::Core::Request') }
+
+sub psgi_ok { [ 200, [], ['OK'] ] }
+
+sub test_get_params {
+ my %exp_params = (
+ 'name' => 'Alexis Sukrieh',
+ 'IRC Nickname' => 'sukria',
+ 'Project' => 'Perl Dancer2',
+ 'hash' => [ 2, 4 ],
+ int1 => 1,
+ int2 => 0,
+ );
+
+ my $test = Plack::Test->create( sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+
+ is( $request->path, '/', 'path is set' );
+ is( $request->method, 'GET', 'method is set' );
+ ok( $request->is_get, 'method is GET' );
+
+ is_deeply(
+ scalar( $request->params ),
+ \%exp_params,
+ 'params are OK',
+ );
+
+ is(
+ $request->params->{'name'},
+ 'Alexis Sukrieh',
+ 'params accessor works',
+ );
+
+ my %params = $request->params;
+ is_deeply(
+ scalar( $request->params ),
+ \%params,
+ 'params wantarray works',
+ );
+
+ return psgi_ok;
+ } );
+
+ my $request_url = '/?' .
+ join '&', map {;
+ my $param = $_;
+ ref $exp_params{$param}
+ ? map +( uri_escape($param).'='.uri_escape($_) ), @{ $exp_params{$_} }
+ : uri_escape($_).'='.uri_escape( $exp_params{$param} );
+ } keys %exp_params;
+
+ ok(
+ $test->request( GET $request_url )->is_success,
+ 'Request successful',
+ );
+}
+
+sub test_post_params {
+ my %exp_params = (
+ foo => 'bar',
+ name => 'john',
+ hash => [ 2, 4, 6 ],
+ );
+
+ my $test = Plack::Test->create( sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+
+ is( $request->path, '/', 'path is set' );
+ is( $request->method, 'POST', 'method is set' );
+ ok( $request->is_post, 'method is POST' );
+
+ like(
+ $request->to_string,
+ qr{^\[\#\d+\] POST /},
+ 'Request presented well as string',
+ );
+
+ is_deeply(
+ scalar( $request->params ),
+ \%exp_params,
+ 'params are OK',
+ );
+
+ my %params = $request->params;
+ is_deeply(
+ scalar( $request->params ),
+ \%params,
+ 'params wantarray works',
+ );
+
+ is_deeply(
+ scalar( $request->params ),
+ \%params,
+ 'params wantarray works',
+ );
+
+ return psgi_ok;
+ } );
+
+ my $req = POST '/', \%exp_params;
+
+ ok(
+ $test->request($req)->is_success,
+ 'Request successful',
+ );
+}
+
+sub test_mixed_params {
+ my $test = Plack::Test->create( sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+
+ my %exp_params = (
+ mixed => {
+ x => 1, y => 2, meth => 'post',
+ },
+
+ get => {
+ y => 2, meth => 'get',
+ },
+
+ post => {
+ x => 1, meth => 'post',
+ },
+ );
+
+ is( $request->path, '/', 'path is set' );
+ is( $request->method, 'POST', 'method is set' );
+
+ is_deeply(
+ scalar( $request->params ),
+ $exp_params{'mixed'},
+ 'params are OK',
+ );
+
+ is_deeply(
+ scalar( $request->params('body') ),
+ $exp_params{'post'},
+ 'body params are OK',
+ );
+
+ is_deeply(
+ scalar( $request->params('query') ),
+ $exp_params{'get'},
+ 'query params are OK',
+ );
+
+ return psgi_ok;
+ } );
+
+ my $req = POST '/?y=2&meth=get',
+ { x => 1, meth => 'post' };
+
+ ok(
+ $test->request($req)->is_success,
+ 'Request successful',
+ );
+}
+
+sub test_all_params {
+ test_get_params;
+ test_post_params;
+ test_mixed_params;
+}
+
+subtest 'Defaults' => sub {
+ my $test = Plack::Test->create( sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+ isa_ok( $request, 'Dancer2::Core::Request' );
+
+ can_ok( $request, 'env' );
+ isa_ok( $request->env, 'HASH' );
+
+ # http env keys
+ my @http_env_keys = qw<
+ accept accept_charset accept_encoding accept_language
+ connection keep_alive referer user_agent x_requested_with
+ >;
+
+ can_ok( $request, @http_env_keys );
+
+ is(
+ $request->$_,
+ $request->env->{"HTTP_$_"},
+ "HTTP ENV key $_",
+ ) for @http_env_keys;
+
+ is(
+ $request->agent,
+ $request->user_agent,
+ 'agent as alias to user_agent',
+ );
+
+ is(
+ $request->remote_address,
+ $request->address,
+ 'remote_address as alias to address',
+ );
+
+ # variables
+ $request->var( foo => 'bar' );
+ is_deeply(
+ $request->vars,
+ { foo => 'bar' },
+ 'Setting variables using DSL',
+ );
+
+ is( $request->var('foo'), 'bar', 'Read single variable' );
+
+ $request->var( foo => 'baz' );
+
+ is_deeply(
+ $request->vars,
+ { foo => 'baz' },
+ 'Overwriting variables using vars() method',
+ );
+
+ is( $request->var('foo'), 'baz', 'Read variable' );
+
+ is( $request->path, '/defaults', 'Default path' );
+ is( $request->path_info, '/defaults', 'Default path_info' );
+ is( $request->method, 'GET', 'Default method' );
+
+ is( $request->id, 1, 'Correct request ID' );
+
+ my %aliases = (
+ address => 'REMOTE_ADDR',
+ remote_host => 'REMOTE_HOST',
+ protocol => 'SERVER_PROTOCOL',
+ port => 'SERVER_PORT',
+ request_uri => 'REQUEST_URI',
+ user => 'REMOTE_USER',
+ script_name => 'SCRIPT_NAME',
+ );
+
+ is(
+ $request->$_,
+ $request->env->{ $aliases{$_} },
+ "$_ derived from $aliases{$_}",
+ ) for keys %aliases;
+
+ is(
+ $request->to_string,
+ '[#1] GET /defaults',
+ 'Correct to_string',
+ );
+
+ return psgi_ok;
+ } );
+
+ ok(
+ $test->request( GET '/defaults' )->is_success,
+ 'Request successful',
+ );
+};
+
+subtest 'Create with single env' => sub {
+ isa_ok(
+ Dancer2::Core::Request->new( env => {} ),
+ 'Dancer2::Core::Request',
+ 'Create with env hash',
+ );
+
+ my $request;
+ isa_ok(
+ $request = Dancer2::Core::Request->new(
+ env => { REQUEST_METHOD => 'X' }
+ ),
+ 'Dancer2::Core::Request',
+ 'Create with single argument for env',
+ );
+
+ is( $request->method, 'X', 'env() attribute populated successfully' );
+};
+
+subtest 'Serializer' => sub {
+ {
+ my $request = Dancer2::Core::Request->new( env => {} );
+ can_ok( $request, qw<serializer> );
+ ok( ! $request->serializer, 'No serializer set' );
+ }
+
+ {
+ { package Nothing; use Moo; }
+
+ # The type check fails - BUILD is not called, no increment of _count.
+ ok(
+ exception {
+ Dancer2::Core::Request->new(
+ env => {},
+ serializer => Nothing->new,
+ )
+ },
+ 'Cannot send random object to request as serializer',
+ );
+
+ {
+ package Serializer;
+ use Moo;
+ with 'Dancer2::Core::Role::Serializer';
+ sub serialize {1}
+ sub deserialize {1}
+ has '+content_type' => ( default => sub {1} );
+ }
+
+ my $request;
+ is(
+ exception {
+ $request = Dancer2::Core::Request->new(
+ env => { REQUEST_METHOD => 'GET' },
+ serializer => Serializer->new,
+ )
+ },
+ undef,
+ 'Can create request with serializer',
+ );
+
+ ok( $request->serializer, 'Serializer set' );
+ isa_ok( $request->serializer, 'Serializer' );
+ }
+};
+
+subtest 'Path when mounting' => sub {
+ my $app = builder { mount '/mount' => sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+
+ is(
+ $request->script_name,
+ '/mount',
+ 'Script name when mounted (script_name)',
+ );
+
+ is(
+ $request->request_uri,
+ '/mount/mounted_path',
+ 'Correct request_uri',
+ );
+
+ is(
+ $request->path,
+ '/mounted_path',
+ 'Full path when mounted (path)',
+ );
+
+ is(
+ $request->path_info,
+ '/mounted_path',
+ 'Mounted path when mounted (path_info)',
+ );
+
+ return psgi_ok;
+ } };
+
+ my $test = Plack::Test->create($app);
+
+ ok(
+ $test->request( GET '/mount/mounted_path' )->is_success,
+ 'Request successful',
+ );
+};
+
+subtest 'Different method' => sub {
+ my $test = Plack::Test->create( sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+
+ is( $request->method, 'PUT', 'Correct method' );
+
+ is(
+ $request->env->{'REQUEST_METHOD'},
+ $request->method,
+ 'REQUEST_METHOD derived from env',
+ );
+
+ return psgi_ok;
+ } );
+
+ ok(
+ $test->request( PUT '/' )->is_success,
+ 'Request successful',
+ );
+};
+
+# the calling order to this method matters because it checks
+# how many requests were run so far
+subtest 'Checking request ID' => sub {
+ my $test = Plack::Test->create( sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+ is( $request->id, 8, 'Correct request id' );
+
+ return psgi_ok;
+ } );
+
+ ok(
+ $test->request( GET '/' )->is_success,
+ 'Request successful',
+ );
+};
+
+subtest 'is_$method (head/post/get/put/delete/patch' => sub {
+ foreach my $http_method ( qw<head post get put delete patch> ) {
+ my $test = Plack::Test->create( sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+ my $method = "is_$http_method";
+ ok( $request->$method, $method );
+ return psgi_ok;
+ } );
+
+ ok(
+ $test->request(
+ HTTP::Request->new( ( uc $http_method ) => '/' )
+ )->is_success,
+ 'Request successful',
+ );
+ }
+};
+
+subtest 'Parameters (body/query/route)' => sub {
+ note $Dancer2::Core::Request::XS_URL_DECODE ?
+ 'Running test with XS_URL_DECODE' :
+ 'Running test without XS_URL_DECODE';
+
+ note $Dancer2::Core::Request::XS_PARSE_QUERY_STRING ?
+ 'Running test with XS_PARSE_QUERY_STRING' :
+ 'Running test without XS_PARSE_QUERY_STRING';
+
+ test_all_params;
+
+ if ( $Dancer2::Core::Request::XS_PARSE_QUERY_STRING ) {
+ note 'Running test without XS_PARSE_QUERY_STRING';
+ $Dancer2::Core::Request::XS_PARSE_QUERY_STRING = 0;
+ test_all_params;
+ }
+
+ if ( $Dancer2::Core::Request::XS_URL_DECODE ) {
+ note 'Running test without XS_URL_DECODE';
+ $Dancer2::Core::Request::XS_URL_DECODE = 0;
+ test_all_params;
+ }
+};
+
+# multiple parsing of request body (such as from creating two request objects)
+# previously caused an infinite loop when HTTP::Body was used.
+subtest 'Multiple request object creation doesnt reparse request body' => sub {
+ my $test = Plack::Test->create( sub {
+ my $env = shift;
+ my $request = Dancer2::Core::Request->new( env => $env );
+ # Second institation shouldn't reparse request body.
+ my $request2 = Dancer2::Core::Request->new( env => $env );
+
+ my %exp_params = (
+ post => {
+ x => 1, meth => 'post',
+ },
+ );
+
+ is( $request->path, '/', 'path is set' );
+ is( $request->method, 'POST', 'method is set' );
+
+ is_deeply(
+ scalar( $request->params('body') ),
+ $exp_params{'post'},
+ 'body params are OK',
+ );
+
+ return psgi_ok;
+ } );
+
+ my $req = POST '/', { x => 1, meth => 'post' };
+
+ ok(
+ $test->request($req)->is_success,
+ 'Request successful',
+ );
+};
+
+# more stuff to test
+
+# special methods:
+# forwarded_for_address
+# forwarded_protocol
+# forwarded_host
+# host
+
+#subtest 'Behind proxy (host/is_behind_proxy)' => sub {
+# my $test = Plack::Test->create( sub { psgi_ok } );
+#
+# ok(
+# $test->request( GET '/dev/null' )->is_success,
+# 'Different method request successful',
+# );
+#};
+
+#subtest 'Path resolution methods' => sub {
+# my $test = Plack::Test->create( sub {
+# my $env = shift;
+# my $request = Dancer2::Core::Request->new( env => $env );
+#
+# return psgi_ok;
+# } );
+#};
+
+#subtest 'Upload' => sub {1};
+#subtest 'Scheme' => sub {1};
+#subtest 'Cookies' => sub {1};
+#subtest 'Headers' => sub {1};
diff --git a/t/classes/Dancer2-Core-Request/serializers.t b/t/classes/Dancer2-Core-Request/serializers.t
new file mode 100644
index 00000000..02591bc2
--- /dev/null
+++ b/t/classes/Dancer2-Core-Request/serializers.t
@@ -0,0 +1,74 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ package App::CBOR; ## no critic
+ use Dancer2;
+
+ # postpone
+ sub setup {
+ set serializer => 'CBOR';
+ post '/' => sub {
+ ::is_deeply( +{ params() }, +{}, 'Empty parameters' );
+ ::is( request->data, 'Foo', 'Correct data using request->data' );
+ return 'ok';
+ };
+ }
+}
+
+subtest 'Testing with CBOR' => sub {
+ eval { require CBOR::XS; 1; }
+ or plan skip_all => 'CBOR::XS is needed for this test';
+
+ eval { require Dancer2::Serializer::CBOR; 1; }
+ or plan skip_all => 'Dancer2::Serializer::CBOR is needed for this test';
+
+ App::CBOR->setup;
+ my $app = Plack::Test->create( App::CBOR->to_app );
+ my $res = $app->request(
+ POST '/',
+ Content => CBOR::XS::encode_cbor('Foo'),
+ );
+
+ ok( $res->is_success, 'Successful response' );
+ is(
+ $res->content,
+ CBOR::XS::encode_cbor('ok'),
+ 'Correct response',
+ );
+};
+
+{
+ package App::JSON; ## no critic
+ use Dancer2;
+ set serializer => 'JSON';
+ post '/' => sub {
+ ::is_deeply( +{ params() }, +{}, 'Empty parameters' );
+ ::is_deeply(
+ request->data,
+ [ qw<foo bar> ],
+ 'Correct data using request->data',
+ );
+ return [ qw<foo bar> ];
+ };
+}
+
+subtest 'Testing with JSON' => sub {
+ my $app = Plack::Test->create( App::JSON->to_app );
+ my $res = $app->request(
+ POST '/',
+ Content => '["foo","bar"]',
+ );
+
+ ok( $res->is_success, 'Successful response' );
+ is(
+ $res->content,
+ '["foo","bar"]',
+ 'Correct response',
+ );
+};
+
+done_testing();
diff --git a/t/classes/Dancer2-Core-Response-Delayed/after_hooks.t b/t/classes/Dancer2-Core-Response-Delayed/after_hooks.t
new file mode 100644
index 00000000..a13c7f05
--- /dev/null
+++ b/t/classes/Dancer2-Core-Response-Delayed/after_hooks.t
@@ -0,0 +1,64 @@
+use strict;
+use warnings;
+use Test::More;
+
+use Plack::Test;
+use HTTP::Request::Common;
+use HTTP::Cookies;
+
+# Tests to ensure a delayed ( but not async ) response
+# still have "after" hooks called, such as for session flushing
+
+{
+ package App::Delayed;
+ use Dancer2;
+
+ set session => 'Simple',
+
+ get '/' => sub {
+ session file => __FILE__;
+ open my $fh, "<", __FILE__;
+ delayed {
+ my $responder = $Dancer2::Core::Route::RESPONDER;
+ my $res = $Dancer2::Core::Route::RESPONSE;
+ return $responder->(
+ [ $res->status, $res->headers_to_array, $fh ]
+ );
+ };
+ };
+
+ get '/file' => sub {
+ session 'file';
+ };
+
+}
+
+my $jar = HTTP::Cookies->new();
+my $base = 'http://localhost';
+
+my $test = Plack::Test->create( App::Delayed->to_app );
+
+subtest "delayed (not async) response" => sub {
+ my $res = $test->request( GET "$base/" );
+ $jar->extract_cookies($res);
+
+ ok $res->is_success, 'Successful request for /';
+
+ open my $fh, "<:raw", __FILE__;
+ my $content = do { local $/; <$fh> };
+ is $res->content, $content, "response returned test file content";
+};
+
+subtest "after hook flushes session headers for delayed response" => sub {
+ my $req = GET("$base/file");
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ $jar->extract_cookies($res);
+
+ ok $res->is_success, 'Successful request for /file';
+ is $res->content, __FILE__, "Session returned test file name";
+};
+
+done_testing();
+
diff --git a/t/classes/Dancer2-Core-Response-Delayed/new.t b/t/classes/Dancer2-Core-Response-Delayed/new.t
new file mode 100644
index 00000000..3eaa93a7
--- /dev/null
+++ b/t/classes/Dancer2-Core-Response-Delayed/new.t
@@ -0,0 +1,70 @@
+use strict;
+use warnings;
+use Test::More tests => 18;
+use Dancer2::Core::Runner;
+use Dancer2::Core::Request;
+use Dancer2::Core::Response;
+
+use_ok('Dancer2::Core::Response::Delayed');
+
+my $runner = Dancer2::Core::Runner->new;
+isa_ok( $runner, 'Dancer2::Core::Runner' );
+$Dancer2::runner = $runner;
+
+my $request = Dancer2::Core::Request->new(
+ env => { PATH_INFO => '/foo' },
+);
+isa_ok( $request, 'Dancer2::Core::Request' );
+
+my $response = Dancer2::Core::Response->new();
+isa_ok( $response, 'Dancer2::Core::Response' );
+
+my $test = 0;
+my $del_res = Dancer2::Core::Response::Delayed->new(
+ request => $request,
+ response => $response,
+ cb => sub {
+ ::isa_ok(
+ $Dancer2::Core::Route::REQUEST,
+ 'Dancer2::Core::Request',
+ );
+
+ ::isa_ok(
+ $Dancer2::Core::Route::RESPONSE,
+ 'Dancer2::Core::Response',
+ );
+
+ ::is(
+ $Dancer2::Core::Route::REQUEST->path,
+ '/foo',
+ 'Correct path in the request',
+ );
+
+ ::isa_ok(
+ $Dancer2::Core::Route::RESPONDER,
+ 'CODE',
+ 'Got a responder callback',
+ );
+
+ $test++;
+
+ $Dancer2::Core::Route::RESPONDER->('OK');
+ },
+);
+
+isa_ok( $del_res, 'Dancer2::Core::Response::Delayed' );
+can_ok( $del_res, qw<request response cb> );
+can_ok( $del_res, qw<is_halted has_passed to_psgi> );
+
+is( $del_res->is_halted, 0, 'is_halted returns no' );
+is( $del_res->has_passed, 0, 'has_passed returns no' );
+
+my $res_cb = sub { is( $_[0], 'OK', 'Correct response asynchronously' ) };
+
+my $psgi_res = $del_res->to_psgi();
+is( $test, 0, 'Callback not run yet' );
+$psgi_res->($res_cb);
+is( $test, 1, 'Callback run' );
+
+is $del_res->status => 200, "we can access the response header";
+isa_ok( $del_res->headers, "HTTP::Headers", "Able to retrieve headers");
diff --git a/t/classes/Dancer2-Core-Response/new_from.t b/t/classes/Dancer2-Core-Response/new_from.t
new file mode 100644
index 00000000..14cc8cbb
--- /dev/null
+++ b/t/classes/Dancer2-Core-Response/new_from.t
@@ -0,0 +1,71 @@
+use strict;
+use warnings;
+use Test::More tests => 3;
+use Plack::Response;
+use Dancer2::Core::Response;
+
+sub normalize_headers {
+ my $headers = shift;
+
+ my %headers = ();
+ while ( my ( $key, $val ) = splice @{$headers}, 0, 2 ) {
+ $headers{$key} = $val;
+ }
+
+ return %headers;
+}
+
+can_ok( Dancer2::Core::Response::, qw<new_from_array new_from_plack> );
+
+my %default_headers = (
+ 'Content-Type' => 'text/plain',
+ 'X-Test' => 'Val',
+);
+
+subtest 'new_from_array' => sub {
+ plan tests => 4;
+
+ my $array = [ 200, [%default_headers], ['Foo'] ];
+ my $response = Dancer2::Core::Response->new_from_array($array);
+
+ isa_ok( $response, 'Dancer2::Core::Response' );
+ is( $response->status, 200, 'Correct status' );
+ is( $response->content, 'Foo', 'Correct content' );
+
+ # hash randomization
+ my %headers = normalize_headers( $response->headers_to_array );
+
+ is_deeply(
+ \%headers,
+ \%default_headers,
+ 'All headers correct',
+ );
+};
+
+subtest 'new_from_plack' => sub {
+ plan tests => 5;
+
+ my $plack = Plack::Response->new();
+ isa_ok( $plack, 'Plack::Response' );
+
+ $plack->status(200);
+ $plack->body('Bar');
+ foreach my $header_name ( keys %default_headers ) {
+ $plack->header( $header_name => $default_headers{$header_name} );
+ }
+
+ my $response = Dancer2::Core::Response->new_from_plack($plack);
+ isa_ok( $response, 'Dancer2::Core::Response' );
+ is( $response->status, 200, 'Correct status' );
+ is( $response->content, 'Bar', 'Correct content' );
+
+ # hash randomization
+ my %headers = normalize_headers( $response->headers_to_array );
+
+ is_deeply(
+ \%headers,
+ \%default_headers,
+ 'All headers correct',
+ );
+};
+
diff --git a/t/classes/Dancer2-Core-Role-Engine/with.t b/t/classes/Dancer2-Core-Role-Engine/with.t
new file mode 100644
index 00000000..2ad3281d
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-Engine/with.t
@@ -0,0 +1,21 @@
+use strict;
+use warnings;
+use Test::More tests => 4;
+
+{
+ package App;
+ use Moo;
+ with 'Dancer2::Core::Role::Engine';
+ sub hook_aliases { +{} }
+ sub supported_hooks {}
+}
+
+my $app = App->new;
+isa_ok( $app, 'App' );
+can_ok( $app, qw<session config> ); # attributes
+can_ok( $app, qw<set_session clear_session has_session> ); # methods
+ok(
+ $app->DOES('Dancer2::Core::Role::Hookable'),
+ 'App consumes Dancer2::Core::Role::Hookable',
+);
+
diff --git a/t/classes/Dancer2-Core-Role-Handler/with.t b/t/classes/Dancer2-Core-Role-Handler/with.t
new file mode 100644
index 00000000..b1905497
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-Handler/with.t
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+use Test::More tests => 3;
+
+{
+ package Handler;
+ use Moo;
+ with 'Dancer2::Core::Role::Handler';
+ sub register {}
+}
+
+my $handler = Handler->new;
+isa_ok( $handler, 'Handler' );
+can_ok( $handler, qw<app> ); # attributes
+ok(
+ $handler->DOES('Dancer2::Core::Role::Handler'),
+ 'Handler consumes Dancer2::Core::Role::Handler',
+);
+
diff --git a/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists b/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/bin/.exists
diff --git a/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists b/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir/lib/fake/inner/dir/.exists
diff --git a/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer b/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/.dancer
diff --git a/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl b/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-HasLocation/FakeDancerFile/fakescript.pl
diff --git a/t/classes/Dancer2-Core-Role-HasLocation/with.t b/t/classes/Dancer2-Core-Role-HasLocation/with.t
new file mode 100644
index 00000000..ab80b84b
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-HasLocation/with.t
@@ -0,0 +1,105 @@
+use strict;
+use warnings;
+use File::Spec;
+use File::Basename;
+use Test::More tests => 11;
+
+{
+ package App;
+ use Moo;
+ with 'Dancer2::Core::Role::HasLocation';
+}
+
+note 'Defaults:'; {
+ my $app = App->new();
+ isa_ok( $app, 'App' );
+ can_ok( $app, qw<caller location> ); # attributes
+ can_ok( $app, '_build_location' ); # methods
+
+ ok(
+ $app->DOES('Dancer2::Core::Role::HasLocation'),
+ 'App consumes Dancer2::Core::Role::HasLocation',
+ );
+
+ my $path = File::Spec->catfile(qw<
+ t classes Dancer2-Core-Role-HasLocation with.t
+ >);
+
+ is(
+ File::Spec->canonpath( $app->caller ),
+ $path,
+ 'Default caller',
+ );
+
+}
+
+my $basedir = dirname( File::Spec->rel2abs(__FILE__) );
+
+note 'With lib/ and bin/:'; {
+ my $app = App->new(
+ caller => File::Spec->catfile(
+ $basedir, qw<FakeDancerDir fake inner dir fakescript.pl>
+ )
+ );
+
+ isa_ok( $app, 'App' );
+
+ my $location = $app->location;
+ $location =~ s/\/$//;
+
+ my $path = File::Spec->rel2abs(
+ File::Spec->catdir(
+ File::Spec->curdir,
+ qw<t classes Dancer2-Core-Role-HasLocation FakeDancerDir>,
+ )
+ );
+
+ is(
+ $location,
+ $path,
+ 'Got correct location with lib/ and bin/',
+ );
+}
+
+note 'With .dancer file:'; {
+ my $app = App->new(
+ caller => File::Spec->catfile(
+ $basedir, qw<FakeDancerFile script.pl>
+ )
+ );
+
+ isa_ok( $app, 'App' );
+
+ my $location = $app->location;
+
+ my $path = File::Spec->rel2abs(
+ File::Spec->catdir(
+ File::Spec->curdir,
+ qw<t classes Dancer2-Core-Role-HasLocation FakeDancerFile>,
+ )
+ );
+
+ is( $location, $path, 'Got correct location with .dancer file' );
+}
+
+note 'blib/ ignored:'; {
+ my $app = App->new(
+ caller => File::Spec->catfile(
+ $basedir, qw<FakeDancerDir blib lib fakescript.pl>
+ )
+ );
+
+ isa_ok( $app, 'App' );
+
+ my $location = $app->location;
+ $location =~ s/\/$//;
+
+ my $path = File::Spec->rel2abs(
+ File::Spec->catdir(
+ File::Spec->curdir,
+ qw<t classes Dancer2-Core-Role-HasLocation FakeDancerDir>,
+ )
+ );
+
+ is( $location, $path, 'blib/ dir is ignored' );
+}
diff --git a/t/classes/Dancer2-Core-Role-Serializer/with.t b/t/classes/Dancer2-Core-Role-Serializer/with.t
new file mode 100644
index 00000000..4524079d
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-Serializer/with.t
@@ -0,0 +1,164 @@
+use strict;
+use warnings;
+use Test::More tests => 4;
+use Test::Fatal;
+
+use_ok('Dancer2::Core::Hook');
+
+{
+ package Serializer::OK;
+ use Moo;
+ with 'Dancer2::Core::Role::Serializer';
+ has '+content_type' => ( default => sub {'plain/test'} );
+
+ sub serialize {'{'.$_[1].'}'}
+ sub deserialize {'['.$_[1].']'}
+}
+
+subtest 'Successful' => sub {
+ plan tests => 5;
+
+ my $srl = Serializer::OK->new;
+ isa_ok( $srl, 'Serializer::OK' );
+
+ $srl->add_hook(
+ Dancer2::Core::Hook->new(
+ name => 'engine.serializer.before',
+ code => sub {
+ my $content = shift;
+ ::is( $content, 'foo', 'Correct content in before hook' );
+ },
+ )
+ );
+
+ $srl->add_hook(
+ Dancer2::Core::Hook->new(
+ name => 'engine.serializer.after',
+ code => sub {
+ my $content = shift;
+ ::is( $content, '{foo}', 'Correct content in after hook' );
+ },
+ )
+ );
+
+ is( $srl->serialize('foo'), '{foo}', 'Serializing' );
+ is( $srl->deserialize('bar'), '[bar]', 'Deserializing' );
+};
+
+{
+ package Serializer::NotOK;
+ use Moo;
+ with 'Dancer2::Core::Role::Serializer';
+ has '+content_type' => ( default => sub {'plain/test'} );
+
+ sub serialize { die '+' . $_[1] . '+' }
+ sub deserialize { die '-' . $_[1] . '-' }
+}
+
+subtest 'Unsuccessful' => sub {
+ plan tests => 21;
+
+ use_ok('Dancer2::Logger::Capture');
+
+ {
+ my $logger = Dancer2::Logger::Capture->new;
+ isa_ok( $logger, 'Dancer2::Logger::Capture' );
+
+ my $srl = Serializer::NotOK->new(
+ log_cb => sub { $logger->log(@_) }
+ );
+
+ isa_ok( $srl, 'Serializer::NotOK' );
+ is( $srl->serialize('foo'), undef, 'Serialization result' );
+
+ 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];
+ isa_ok( $msg, 'HASH' );
+ is( scalar keys %{$msg}, 3, 'Two items in the error' );
+
+ is( $msg->{'level'}, 'core', 'Correct level' );
+ like(
+ $msg->{'message'},
+ qr{^Failed to serialize content: \+foo\+},
+ 'Correct error message',
+ );
+ }
+
+ {
+ my $logger = Dancer2::Logger::Capture->new;
+ isa_ok( $logger, 'Dancer2::Logger::Capture' );
+
+ my $srl = Serializer::NotOK->new(
+ log_cb => sub { $logger->log(@_) }
+ );
+
+ isa_ok( $srl, 'Serializer::NotOK' );
+ is( $srl->deserialize('bar'), undef, 'Deserialization result' );
+
+ 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];
+ isa_ok( $msg, 'HASH' );
+ is( scalar keys %{$msg}, 3, 'Two items in the error' );
+
+ is( $msg->{'level'}, 'core', 'Correct level' );
+ like(
+ $msg->{'message'},
+ qr{^Failed to deserialize content: \-bar\-},
+ 'Correct error message',
+ );
+ }
+};
+
+{
+ package Serializer::Empty;
+ use Moo;
+ with 'Dancer2::Core::Role::Serializer';
+ has '+content_type' => ( default => 'plain/test' );
+ sub serialize {'BAD SERIALIZE'}
+ sub deserialize {'BAD DESERIALIZE'}
+}
+
+subtest 'Called with empty content' => sub {
+ plan tests => 6;
+
+ my $srl = Serializer::Empty->new;
+ isa_ok( $srl, 'Serializer::Empty' );
+ can_ok( $srl, qw<serialize deserialize> );
+
+ is(
+ $srl->serialize(),
+ undef,
+ 'Do not try to serialize without input',
+ );
+
+ is(
+ $srl->serialize(''),
+ '',
+ 'Do not try to serialize with empty input',
+ );
+
+ is(
+ $srl->deserialize(),
+ undef,
+ 'Do not try to deserialize without input',
+ );
+
+ is(
+ $srl->deserialize(''),
+ '',
+ 'Do not try to deserialize with empty input',
+ );
+}
+
diff --git a/t/classes/Dancer2-Core-Role-StandardResponses/with.t b/t/classes/Dancer2-Core-Role-StandardResponses/with.t
new file mode 100644
index 00000000..4bba4e56
--- /dev/null
+++ b/t/classes/Dancer2-Core-Role-StandardResponses/with.t
@@ -0,0 +1,120 @@
+use strict;
+use warnings;
+use Test::More tests => 24;
+
+{
+ package Handler;
+ use Moo;
+ with 'Dancer2::Core::Role::StandardResponses';
+}
+
+{
+ package App;
+ use Moo;
+ has response => ( is => 'ro', default => sub { Response->new } );
+}
+
+{
+ package Response;
+ use Moo;
+ has status => (is => 'ro', reader => '_status');
+ has header => (is => 'ro', reader => '_header');
+ sub status { shift->_status->(@_) }
+ sub header { shift->_header->(@_) }
+}
+
+note 'Checking our fake app'; {
+ my $app = App->new;
+ isa_ok( $app, 'App' );
+ can_ok( $app, 'response' );
+ isa_ok( $app->response, 'Response' );
+}
+
+note 'Checking our fake response'; {
+ my $response = Response->new(
+ status => sub {
+ my ( $self, $input ) = @_;
+ ::isa_ok( $self, 'Response' );
+ ::is( $input, 'calling status', 'status called' );
+ return 'foo';
+ },
+
+ header => sub {
+ my ( $self, $input ) = @_;
+ ::isa_ok( $self, 'Response' );
+ ::is( $input, 'calling header', 'header called' );
+ return qw<bar baz>;
+ },
+ );
+
+ isa_ok( $response, 'Response' );
+
+ is_deeply(
+ [ $response->status('calling status') ],
+ [ 'foo' ],
+ 'status() works',
+ );
+
+ is_deeply(
+ [ $response->header('calling header') ],
+ [ qw<bar baz> ],
+ 'header() works',
+ );
+}
+
+my $handler = Handler->new;
+isa_ok( $handler, 'Handler' );
+can_ok( $handler, qw<response standard_response> );
+
+note '->response'; {
+ # set up status and header
+ my $app = App->new(
+ response => Response->new(
+ status => sub {
+ my ( $self, $code ) = @_;
+ ::isa_ok( $self, 'Response' );
+ ::is( $code, '400', 'Correct status code' );
+ },
+
+ header => sub {
+ my ( $self, $hdr_name, $hdr_content ) = @_;
+ ::isa_ok( $self, 'Response' );
+ ::is( $hdr_name, 'Content-Type', 'Correct header name' );
+ ::is( $hdr_content, 'text/plain', 'Correct header value' );
+ },
+ )
+ );
+
+ is(
+ $handler->response( $app, 400, 'Some Message' ),
+ 'Some Message',
+ 'Correct response created',
+ );
+}
+
+note '->standard_response'; {
+ # set up status and header
+ my $app = App->new(
+ response => Response->new(
+ status => sub {
+ my ( $self, $code ) = @_;
+ ::isa_ok( $self, 'Response' );
+ ::is( $code, '400', 'Correct status code' );
+ },
+
+ header => sub {
+ my ( $self, $hdr_name, $hdr_content ) = @_;
+ ::isa_ok( $self, 'Response' );
+ ::is( $hdr_name, 'Content-Type', 'Correct header name' );
+ ::is( $hdr_content, 'text/plain', 'Correct header value' );
+ },
+ )
+ );
+
+ is(
+ $handler->standard_response( $app, 400 ),
+ 'Bad Request',
+ 'Correct response 400 created',
+ );
+}
+
diff --git a/t/classes/Dancer2-Core-Route/base.t b/t/classes/Dancer2-Core-Route/base.t
new file mode 100644
index 00000000..223ba6ae
--- /dev/null
+++ b/t/classes/Dancer2-Core-Route/base.t
@@ -0,0 +1,67 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Fatal;
+use Dancer2::Core::Route;
+
+plan tests => 3;
+
+my @no_leading_slash = ( 'no+leading+slash', '' );
+my @leading_slash = ('/+leading+slash', '/', '//' );
+
+subtest "no prefix, paths without a leading slash" => sub {
+ for my $string ( @no_leading_slash ) {
+ my $route;
+ my $exception = exception {
+ $route = Dancer2::Core::Route->new(
+ regexp => $string,
+ method => 'get',
+ code => sub {1},
+ );
+ };
+ is( $exception, undef,
+ "'$string' is a valid route pattern"
+ );
+ is( $route->spec_route, "/$string",
+ "undef prefix prepends '/' to spec_route"
+ );
+ }
+};
+
+subtest "no prefix, paths with a leading slash" => sub {
+ for my $string ( @leading_slash ) {
+ my $route;
+ my $exception = exception {
+ $route = Dancer2::Core::Route->new(
+ regexp => $string,
+ method => 'get',
+ code => sub {1},
+ );
+ };
+ is( $exception, undef,
+ "'$string' is a valid route pattern"
+ );
+ is( $route->spec_route, $string,
+ "undef prefix does not prepend '/' to spec_route"
+ );
+ }
+};
+
+subtest "prefix and paths append" => sub {
+ my $prefix = '/prefix';
+ for my $string ( @no_leading_slash, @leading_slash) {
+ my $route;
+ my $exception = exception {
+ $route = Dancer2::Core::Route->new(
+ regexp => $string,
+ prefix => $prefix,
+ method => 'get',
+ code => sub {1},
+ );
+ };
+ is( $exception, undef,
+ "'$prefix$string' is a valid route pattern"
+ );
+ }
+};
+
diff --git a/t/classes/Dancer2-Core-Route/deprecated_param_keys.t b/t/classes/Dancer2-Core-Route/deprecated_param_keys.t
new file mode 100644
index 00000000..33c8a879
--- /dev/null
+++ b/t/classes/Dancer2-Core-Route/deprecated_param_keys.t
@@ -0,0 +1,34 @@
+use strict;
+use warnings;
+use Test::More;
+use Capture::Tiny 0.12 'capture_stderr';
+BEGIN { use_ok('Dancer2::Core::Route') }
+
+like(
+ capture_stderr {
+ Dancer2::Core::Route->new(
+ regexp => '/:splat',
+ code => sub {1},
+ method => 'get',
+ );
+ },
+ qr{^Named placeholder 'splat' is deprecated},
+ 'Find deprecation of :splat',
+);
+
+SKIP: {
+ skip 'Need perl >= 5.10', 1 unless $] >= 5.010;
+ like(
+ capture_stderr {
+ Dancer2::Core::Route->new(
+ regexp => '/:captures',
+ code => sub {1},
+ method => 'get',
+ );
+ },
+ qr{^Named placeholder 'captures' is deprecated},
+ 'Find deprecation of :captures',
+ );
+}
+
+done_testing;
diff --git a/t/classes/Dancer2-Core-Route/match.t b/t/classes/Dancer2-Core-Route/match.t
new file mode 100644
index 00000000..26520a8e
--- /dev/null
+++ b/t/classes/Dancer2-Core-Route/match.t
@@ -0,0 +1,437 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Fatal;
+use Dancer2::Core::Request;
+use Dancer2::Core::Route;
+use Capture::Tiny 0.12 'capture_stderr';
+use Ref::Util qw<is_regexpref>;
+use lib 't/lib';
+
+my @tests = (
+ [ [ 'get', '/', sub {11} ], '/', [ {}, 11 ] ],
+ [ [ 'get', '/', sub {11} ],
+ '/failure',
+ [ undef, 11 ]
+ ],
+
+ # token tests
+ [ [ 'get', '/hello/:name', sub {22} ],
+ '/hello/sukria',
+ [ { name => 'sukria' }, 22 ]
+ ],
+ [ [ 'get', '/hello/:name?', sub {22} ],
+ '/hello/',
+ [ { name => undef }, 22 ]
+ ],
+
+ # prefix tests
+ [ [ 'get', '/', sub {33}, '/forum' ],
+ '/forum/',
+ [ {}, 33 ]
+ ],
+ [ [ 'get', '/', sub {33}, '/forum' ],
+ '/forum/',
+ [ {}, 33 ]
+ ],
+ [ [ 'get', '/mywebsite', sub {33}, '/forum' ],
+ '/forum/mywebsite',
+ [ {}, 33 ]
+ ],
+ [ [ 'get', '', sub {'concat'}, '/' ],
+ '/',
+ [ {}, 'concat' ]
+ ],
+
+ # token in prefix tests
+ [ [ 'get', 'name', sub {35}, '/hello/:' ],
+ '/hello/sukria',
+ [ { name => 'sukria' }, 35 ],
+ ],
+
+ [ [ 'get', '/', sub {36}, '/hello/:name' ],
+ '/hello/sukria/',
+ [ { name => 'sukria' }, 36 ],
+ ],
+
+ # splat test
+ [ [ 'get', '/file/*.*', sub {44} ],
+ '/file/dist.ini',
+ [ { splat => [ 'dist', 'ini' ] }, 44 ]
+ ],
+
+ # splat in prefix
+ [ [ 'get', '', sub {42}, '/forum/*'],
+ '/forum/dancer',
+ [ { splat => [ 'dancer' ] }, 42 ]
+ ],
+
+ # megasplat test
+ [ [ 'get', '/file/**/*', sub {44} ],
+ '/file/some/where/42',
+ [ { splat => [ [ 'some', 'where' ], '42' ] }, 44 ]
+ ],
+
+ # megasplat consistently handles multiple slashes
+ [ [ 'get', '/foo/**', sub {'45a'} ],
+ '/foo/bar///baz',
+ [ { splat => [ [ 'bar', '', '', 'baz' ] ] }, '45a' ]
+ ],
+ [ [ 'get', '/foo/**', sub {'45b'} ],
+ '/foo/bar///', # empty trailing path segment
+ [ { splat => [ [ 'bar', '', '', '' ] ] }, '45b' ]
+ ],
+
+ # Optional megasplat test - with a value...
+ [ [ 'get', '/foo/?**?', sub {46} ],
+ '/foo/bar/baz',
+ [ { splat => [ [ 'bar', 'baz' ] ] }, 46 ],
+ ],
+ # ... and without
+ [ [ 'get', '/foo/?**?', sub {47} ],
+ '/foo',
+ [ { splat => [ [ ] ] }, 47 ],
+ ],
+
+ # mixed (mega)splat and tokens
+ [ [ 'get', '/some/:id/**/*', sub {55} ],
+ '/some/where/to/run/and/hide',
+ [ { id => 'where', splat => [ [ 'to', 'run', 'and' ], 'hide' ] }, 55 ]
+ ],
+ [ [ 'get', '/some/*/**/:id?', sub {55} ],
+ '/some/one/to/say/boo/',
+ [ { id => undef, splat => [ 'one', [ 'to', 'say', 'boo' ] ] }, 55 ]
+ ],
+
+ # supplied regex
+ [ [ 'get', qr{stuff(\d+)}, sub {44} ], '/stuff48',
+ [ { splat => [48] }, 44 ]
+ ],
+ [ [ 'get', qr{/stuff(\d+)}, sub {44}, '/foo' ],
+ '/foo/stuff48',
+ [ { splat => [48] }, 44 ],
+ ],
+
+);
+
+
+plan tests => 111;
+
+for my $t (@tests) {
+ my ( $route, $path, $expected ) = @$t;
+
+ if ( is_regexpref($expected) ) {
+ like(
+ exception {
+ my $r = Dancer2::Core::Route->new(
+ method => $route->[0],
+ regexp => $route->[1],
+ code => $route->[2],
+ prefix => $route->[3],
+ );
+ },
+ $expected,
+ "got expected exception for $path",
+ );
+ }
+ else {
+ my $r = Dancer2::Core::Route->new(
+ method => $route->[0],
+ regexp => $route->[1],
+ code => $route->[2],
+ prefix => $route->[3],
+ );
+ isa_ok $r, 'Dancer2::Core::Route';
+
+ my $request = Dancer2::Core::Request->new(
+ env => {
+ PATH_INFO => $path,
+ REQUEST_METHOD => $route->[0],
+ }
+ );
+ my $m;
+ is( capture_stderr { $m = $r->match($request) }, '',
+ "no warnings generated for $path" );
+ is_deeply $m, $expected->[0], "got expected data for '$path'";
+
+ {
+ package App; use Dancer2; ## no critic
+ }
+
+ use Dancer2::Core::App;
+ use Dancer2::Core::Response;
+ my $app = Dancer2::Core::App->new(
+ request => $request,
+ response => Dancer2::Core::Response->new,
+ );
+
+ is $r->execute($app)->content, $expected->[1], "got expected result for '$path'";
+
+ # failing request
+ my $failing_request = Dancer2::Core::Request->new(
+ env => {
+ PATH_INFO => '/something_that_doesnt_exist',
+ REQUEST_METHOD => 'GET',
+ },
+ );
+
+ $m = $r->match($failing_request);
+ is $m, undef, "don't match failing request";
+ }
+}
+
+# captures test
+SKIP: {
+ skip "Need perl >= 5.10", 1 unless $] >= 5.010;
+
+ ## Regexp is parsed in compile time. So, eval with QUOTES to force to parse later.
+ my $route_regex;
+
+ ## no critic
+
+ eval q{
+ $route_regex = qr{/(?<class> user | content | post )/(?<action> delete | find )/(?<id> \d+ )}x;
+ };
+
+ ## use critic
+
+ my $r = Dancer2::Core::Route->new(
+ regexp => $route_regex,
+ code => sub {
+ 'ok';
+ },
+ method => 'get',
+ );
+
+ my $request = Dancer2::Core::Request->new(
+ env => {
+ PATH_INFO => '/user/delete/234',
+ REQUEST_METHOD => 'GET',
+ },
+ );
+
+ my $m = $r->match($request);
+
+ is_deeply $m,
+ { captures => {
+ class => 'user',
+ action => 'delete',
+ id => 234
+ }
+ },
+ "named captures work";
+}
+
+note "routes with options"; {
+ my $route_w_options = Dancer2::Core::Route->new(
+ method => 'get',
+ regexp => '/',
+ code => sub {'options'},
+ options => { 'agent' => 'cURL' },
+ );
+
+ my $req = Dancer2::Core::Request->new(
+ path => '/',
+ method => 'get',
+ env => { 'HTTP_USER_AGENT' => 'mozilla' },
+ );
+
+ my $m = $route_w_options->match($req);
+ ok !defined $m, 'Route did not match';
+
+ $req = Dancer2::Core::Request->new(
+ path => '/',
+ method => 'get',
+ env => { 'HTTP_USER_AGENT' => 'cURL' },
+ );
+
+ $m = $route_w_options->match($req);
+ ok defined $m, 'Route matched';
+
+ $route_w_options = Dancer2::Core::Route->new(
+ method => 'get',
+ regexp => '/',
+ code => sub {'options'},
+ options => {
+ 'agent' => 'cURL',
+ 'content_type' => 'foo',
+ },
+ );
+
+ $req = Dancer2::Core::Request->new(
+ path => '/',
+ method => 'get',
+ env => { 'HTTP_USER_AGENT' => 'cURL' },
+ );
+
+ # Check match more than once (each iterator wasn't reset, for loop is ok )
+ $m = $route_w_options->match($req);
+ ok !defined $m, 'More options - Route did not match - test 1';
+ $m = $route_w_options->match($req);
+ ok !defined $m, 'More options - Route did not match - test 2';
+}
+
+subtest "typed route params" => sub {
+ my @tests = (
+ {
+ name => "good type check",
+ route => {
+ regexp => '/some/:id[Int]',
+ },
+ request => '/some/34',
+ match => { id => 34 },
+ },
+ {
+ name => "bad required type check",
+ route => {
+ regexp => '/some/:id[Int]',
+ },
+ request => '/some/bad',
+ },
+ {
+ name => "missing required type check",
+ route => {
+ regexp => '/some/:id[Int]',
+ },
+ request => '/some/',
+ },
+ {
+ name => "optional type check exists",
+ route => {
+ regexp => '/some/:id[Int]?',
+ },
+ request => '/some/34',
+ match => { id => 34 },
+ },
+ {
+ name => "optional type check with bad token",
+ route => {
+ regexp => '/some/:id[Int]?',
+ },
+ request => '/some/bad',
+ },
+ {
+ name => "optional type check with empty token",
+ route => {
+ regexp => '/some/:id[Int]?',
+ },
+ request => '/some/',
+ match => { id => undef },
+ },
+ {
+ name => "optional type check with empty token and optional missing trailing slash",
+ route => {
+ regexp => '/some/?:id[Int]?',
+ },
+ request => '/some',
+ match => { id => undef },
+ },
+ {
+ name => "bad type",
+ route => {
+ regexp => '/some/:id[MyDate]?',
+ exception => qr/MyDate is not a known type constraint/,
+ },
+ request => '/some/foo',
+ match => { id => undef },
+ },
+ {
+ name => "custom type with good match",
+ route => {
+ regexp => '/date/:date[MyDate]',
+ args => { type_library => 'TestTypeLibrary' },
+ },
+ request => '/date/2014-01-01',
+ match => { date => '2014-01-01' },
+ },
+ {
+ name => "custom type with bad match",
+ route => {
+ regexp => '/date/:date[MyDate]',
+ args => { type_library => 'TestTypeLibrary' },
+ },
+ request => '/date/X014-01-01',
+ },
+ {
+ name => "type including type library but no type_library config setting",
+ route => {
+ regexp => '/date/:date[TestTypeLibrary::MyDate]',
+ },
+ request => '/date/2014-01-01',
+ match => { date => '2014-01-01' },
+ },
+ {
+ name => "union of types",
+ route => {
+ regexp => '/date/:date[Int|TestTypeLibrary::MyDate]',
+ },
+ request => '/date/2014-01-01',
+ match => { date => '2014-01-01' },
+ },
+ {
+ name => "union of types checking other type",
+ route => {
+ regexp => '/date/:date[Int|TestTypeLibrary::MyDate]',
+ },
+ request => '/date/2014',
+ match => { date => '2014' },
+ },
+ {
+ name => "multiple typed tokens plus other tokens and splats",
+ route => {
+ regexp => '/:id[Int]/:date[MyDate]/:foo/*/**',
+ args => { type_library => 'TestTypeLibrary' },
+ },
+ request => '/42/2018-11-23/bar/dave/was/here',
+ match => {
+ id => 42,
+ date => '2018-11-23',
+ foo => 'bar',
+ splat => [ 'dave', [ 'was', 'here' ] ],
+ },
+ },
+ );
+
+ for my $test (@tests) {
+ my $method = $test->{route}{method} || 'get';
+
+ my %route_args = (
+ method => $method,
+ regexp => $test->{route}{regexp},
+ code => $test->{route}{code} || sub { 'OK' },
+ $test->{route}{prefix} ? ( prefix => $test->{route}{prefix} ) : (),
+ $test->{route}{args} ? %{ $test->{route}{args} } : (),
+ );
+
+ if ( my $exception = $test->{route}{exception} ) {
+ like exception { Dancer2::Core::Route->new(%route_args) },
+ $exception,
+ "'$test->{name}' throws expected exception in route constructor";
+ next;
+ }
+
+ my $route = Dancer2::Core::Route->new(%route_args);
+ my $request = Dancer2::Core::Request->new(
+ env => {
+ PATH_INFO => $test->{request},
+ REQUEST_METHOD => $method,
+ }
+ );
+
+ my $match;
+ is exception {
+ $match = $route->match($request)
+ }, undef, "'$test->{name}' does not throw an exception";
+
+ my $expected = $test->{match};
+ if ( defined $expected ) {
+ is_deeply $match, $expected,
+ "... and route matched with expected captures"
+ or diag explain $match;
+ }
+ else {
+ ok !defined $match, "... and route did not match"
+ or diag explain $match;
+ }
+ }
+};
diff --git a/t/classes/Dancer2-Core-Runner/environment.t b/t/classes/Dancer2-Core-Runner/environment.t
new file mode 100644
index 00000000..9406c3d9
--- /dev/null
+++ b/t/classes/Dancer2-Core-Runner/environment.t
@@ -0,0 +1,42 @@
+use strict;
+use warnings;
+use Test::More tests => 6;
+use Dancer2::Core::Runner;
+
+# 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();
+ isa_ok( $runner, 'Dancer2::Core::Runner' );
+
+ is(
+ $runner->environment,
+ 'development',
+ 'Default environment',
+ );
+}
+
+{
+ local $ENV{DANCER_ENVIRONMENT} = 'foo';
+ my $runner = Dancer2::Core::Runner->new();
+ isa_ok( $runner, 'Dancer2::Core::Runner' );
+ is(
+ $runner->environment,
+ 'foo',
+ 'Successfully set envinronment using DANCER_ENVIRONMENT',
+ );
+}
+
+{
+ local $ENV{PLACK_ENV} = 'bar';
+ my $runner = Dancer2::Core::Runner->new();
+ isa_ok( $runner, 'Dancer2::Core::Runner' );
+ is(
+ $runner->environment,
+ 'bar',
+ 'Successfully set environment using PLACK_ENV',
+ );
+}
+
diff --git a/t/classes/Dancer2-Core-Runner/new.t b/t/classes/Dancer2-Core-Runner/new.t
new file mode 100644
index 00000000..215d69ad
--- /dev/null
+++ b/t/classes/Dancer2-Core-Runner/new.t
@@ -0,0 +1,232 @@
+use strict;
+use warnings;
+use Test::More tests => 39;
+
+# undefine ENV vars used as defaults for app environment in these tests
+local $ENV{DANCER_ENVIRONMENT};
+local $ENV{PLACK_ENV};
+
+use_ok('Dancer2::Core::Runner');
+
+is( $Dancer2::runner, undef, 'No runner defined in Dancer2 yet' );
+
+{
+ my $runner = Dancer2::Core::Runner->new();
+ isa_ok( $runner, 'Dancer2::Core::Runner' );
+}
+
+note 'MIME types'; {
+ my $runner = Dancer2::Core::Runner->new();
+ can_ok( $runner, 'mime_type' );
+ isa_ok( $runner->mime_type, 'Dancer2::Core::MIME' );
+}
+
+ok( $Dancer2::runner, 'Have a runner (probably) in $Dancer2::runner' );
+isa_ok( $Dancer2::runner, 'Dancer2::Core::Runner', 'Runner now defined' );
+
+note 'BUILD setting $Carp::Verbose';
+{
+ my $runner = Dancer2::Core::Runner->new();
+ is( $runner->config->{'traces'}, 0, 'traces not turned on (default' );
+ is( $Carp::Verbose, 0, 'Carp Verbose not turned on (default)' );
+}
+
+{
+ local $ENV{DANCER_TRACES} = 1;
+ my $runner = Dancer2::Core::Runner->new();
+ is( $runner->config->{'traces'}, 1, 'traces turned on' );
+ is( $Carp::Verbose, 1, 'Carp Verbose turned on (using DANCER_TRACES)' );
+}
+
+note 'server'; {
+ my $runner = Dancer2::Core::Runner->new(
+ host => '1.2.3.4', port => 9543, timeout => 3,
+ );
+ can_ok( $runner, qw<server _build_server run> );
+
+ my $server = $runner->server;
+ isa_ok( $server, 'HTTP::Server::PSGI' );
+ can_ok( $server, 'run' );
+ foreach my $attr ( qw<host port timeout> ) {
+ is( $server->{$attr}, $runner->$attr, "$attr set correctly in Server" );
+ }
+
+ is(
+ $server->{'server_software'},
+ "Perl Dancer2 " . Dancer2->VERSION,
+ 'server_software set correctly in Server',
+ );
+}
+
+note 'Environment';
+{
+ my $runner = Dancer2::Core::Runner->new();
+
+ is(
+ $runner->environment,
+ 'development',
+ 'Default environment',
+ );
+}
+
+{
+ local $ENV{DANCER_ENVIRONMENT} = 'foo';
+ my $runner = Dancer2::Core::Runner->new();
+ is(
+ $runner->environment,
+ 'foo',
+ 'Successfully set envinronment using DANCER_ENVIRONMENT',
+ );
+
+ $runner->config->{'apphandler'} = 'Standalone';
+}
+
+{
+ local $ENV{PLACK_ENV} = 'bar';
+ my $runner = Dancer2::Core::Runner->new();
+ is(
+ $runner->environment,
+ 'bar',
+ 'Successfully set environment using PLACK_ENV',
+ );
+
+ is(
+ $runner->config->{'apphandler'},
+ 'PSGI',
+ 'apphandler set to PSGI under PLACK_ENV',
+ );
+}
+
+{
+ local $ENV{DANCER_APPHANDLER} = 'baz';
+ my $runner = Dancer2::Core::Runner->new();
+ is(
+ $runner->config->{'apphandler'},
+ 'baz',
+ 'apphandler set via DANCER_APPHANDLER',
+ );
+}
+
+note 'Server tokens';
+{
+ my $runner = Dancer2::Core::Runner->new();
+ is(
+ $runner->config->{'no_server_tokens'},
+ 0,
+ 'Default no_server_tokens',
+ );
+}
+
+{
+ local $ENV{DANCER_NO_SERVER_TOKENS} = 1;
+ my $runner = Dancer2::Core::Runner->new();
+ is(
+ $runner->config->{'no_server_tokens'},
+ 1,
+ 'Successfully set no_server_tokens using DANCER_NO_SERVER_TOKENS',
+ );
+}
+
+note 'Startup info';
+{
+ my $runner = Dancer2::Core::Runner->new();
+ is(
+ $runner->config->{'startup_info'},
+ 1,
+ 'Default startup_info',
+ );
+}
+
+{
+ local $ENV{DANCER_STARTUP_INFO} = 0;
+ my $runner = Dancer2::Core::Runner->new();
+ is(
+ $runner->config->{'startup_info'},
+ 0,
+ 'Successfully set startup_info using DANCER_STARTUP_INFO',
+ );
+}
+
+{
+ {
+ package App::Fake;
+ use Moo;
+ has name => (
+ is => 'ro',
+ default => sub {__PACKAGE__},
+ );
+
+ has postponed_hooks => (
+ is => 'ro',
+ default => sub { +{
+ before => 'that',
+ after => 'this',
+ } },
+ );
+ }
+ my $runner = Dancer2::Core::Runner->new();
+ my $app = App::Fake->new();
+ can_ok( $runner, qw<register_application add_postponed_hooks> );
+
+ is_deeply(
+ $runner->apps,
+ [],
+ 'Apps are empty at first',
+ );
+
+ is_deeply(
+ $runner->postponed_hooks,
+ +{},
+ 'No postponed hooks at first',
+ );
+
+ $runner->register_application($app);
+
+ is_deeply(
+ $runner->apps,
+ [$app],
+ 'Runner registered application',
+ );
+
+ is_deeply(
+ $runner->postponed_hooks,
+ { 'App::Fake' => $app->postponed_hooks },
+ 'Runner registered the App\'s postponed hooks',
+ );
+}
+
+{
+ my $runner = Dancer2::Core::Runner->new();
+ can_ok( $runner, qw<start start_server> );
+
+ $runner->config->{'apphandler'} = 'PSGI';
+ $runner->config->{'startup_info'} = 0;
+ my $app = $runner->start;
+ isa_ok( $app, 'CODE' );
+
+ {
+ package Server::Fake;
+ sub new { bless {}, 'Server::Fake' }
+ sub run {
+ my ( $self, $app ) = @_;
+ ::isa_ok( $self, 'Server::Fake' );
+ ::isa_ok( $app, 'CODE' );
+
+ return 'OK';
+ }
+ }
+
+ $runner->{'server'} = Server::Fake->new;
+ my $res = $runner->start_server($app);
+ is( $res, 'OK', 'start_server works' );
+}
+
+{
+ my $runner = Dancer2::Core::Runner->new();
+ can_ok( $runner, 'start' );
+
+ $runner->config->{'apphandler'} = 'PSGI';
+ my $app = $runner->start;
+ isa_ok( $app, 'CODE' );
+}
+
diff --git a/t/classes/Dancer2-Core-Runner/psgi_app.t b/t/classes/Dancer2-Core-Runner/psgi_app.t
new file mode 100644
index 00000000..ea7c1a2b
--- /dev/null
+++ b/t/classes/Dancer2-Core-Runner/psgi_app.t
@@ -0,0 +1,89 @@
+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 );
+ };
+}
+
diff --git a/t/classes/Dancer2-Core/camelize.t b/t/classes/Dancer2-Core/camelize.t
new file mode 100644
index 00000000..ed52f228
--- /dev/null
+++ b/t/classes/Dancer2-Core/camelize.t
@@ -0,0 +1,22 @@
+use strict;
+use warnings;
+use Dancer2::Core;
+use Test::More tests => 4;
+
+my %tests = (
+ 'test' => 'Test',
+ 'class_name' => 'ClassName',
+ 'class_nAME' => 'ClassNAME',
+ 'class_NAME' => 'ClassNAME',
+);
+
+foreach my $test ( keys %tests ) {
+ my $value = $tests{$test};
+
+ is(
+ Dancer2::Core::camelize($test),
+ $value,
+ "$test camelized as $value",
+ );
+}
+
diff --git a/t/classes/Dancer2/import-pragmas.t b/t/classes/Dancer2/import-pragmas.t
new file mode 100644
index 00000000..734d5048
--- /dev/null
+++ b/t/classes/Dancer2/import-pragmas.t
@@ -0,0 +1,18 @@
+use strict;
+use Test::More tests => 1;
+
+{
+ package App::NoWarnings; ## no critic
+ no warnings 'misc'; # masks earlier declaration
+ use Dancer2 ':nopragmas';
+
+ local $@ = undef;
+ my $got_warning;
+
+ local $SIG{'__WARN__'} = sub {
+ $got_warning++;
+ };
+
+ eval 'my $var; my $var;'; ## no critic
+ ::is( $got_warning, undef, 'warnings pragma not activated' );
+}
diff --git a/t/classes/Dancer2/import.t b/t/classes/Dancer2/import.t
new file mode 100644
index 00000000..c9ef3834
--- /dev/null
+++ b/t/classes/Dancer2/import.t
@@ -0,0 +1,270 @@
+use strict;
+use warnings;
+use Test::More tests => 34;
+use Test::Fatal;
+use Scalar::Util 'refaddr';
+use Plack::Test;
+use HTTP::Request::Common;
+
+BEGIN {
+ require Dancer2;
+ can_ok( Dancer2::, 'runner' );
+ is( Dancer2::->runner, undef, 'No runner by default' );
+}
+
+{
+ package App::CreatingRunner;
+ use Dancer2;
+}
+
+isa_ok( Dancer2->runner, 'Dancer2::Core::Runner', 'Runner created' );
+my $runner_refaddr = refaddr( Dancer2->runner );
+
+{
+ package App::NotRecreatingRunner;
+ use Dancer2;
+}
+
+isa_ok( Dancer2->runner, 'Dancer2::Core::Runner', 'Runner created' );
+is( refaddr( Dancer2->runner ), $runner_refaddr, 'Runner not recreated' );
+
+{
+ {
+ package FakeRunner;
+ sub psgi_app {
+ ::isa_ok( $_[0], 'FakeRunner' );
+ ::is( $_[1], 'psgi_param', 'psgi_app calls Runner->psgi_app' );
+ return 'Got it';
+ }
+ }
+
+ local $Dancer2::runner = bless {}, 'FakeRunner';
+ ::is(
+ Dancer2->psgi_app('psgi_param'),
+ 'Got it',
+ 'psgi_app works as expected',
+ );
+}
+
+{
+ package App::ScriptAllowed;
+ require Dancer2;
+
+ ::is(
+ ::exception { Dancer2->import(':script') },
+ undef,
+ ':script is allowed',
+ );
+}
+
+{
+ package App::TestsAllowed;
+ require Dancer2;
+
+ ::is(
+ ::exception { Dancer2->import(':tests') },
+ undef,
+ ':tests is allowed',
+ );
+}
+
+{
+ package App::SyntaxAllowed;
+ require Dancer2;
+
+ ::is(
+ ::exception { Dancer2->import(':syntax') },
+ undef,
+ ':syntax is allowed',
+ );
+}
+
+{
+ package App::KeyPairOnly;
+ require Dancer2;
+
+ ::like(
+ ::exception { Dancer2->import('single') },
+ qr{^parameters must be key/value pairs},
+ 'Must import key/value pairs',
+ );
+
+ ::like(
+ ::exception { Dancer2->import(qw<three items requested>) },
+ qr{^parameters must be key/value pairs},
+ 'Must import key/value pairs',
+ );
+
+ ::is(
+ ::exception { Dancer2->import( '!unless' ) },
+ undef,
+ 'Must import key/value pairs unless prefixed by !',
+ );
+
+ ::is(
+ ::exception { Dancer2->import( '!unless', '!prefixed', '!bythis' ) },
+ undef,
+ 'Must import key/value pairs unless prefixed by !',
+ );
+}
+
+{
+ package App::GettingDSL;
+ use Dancer2;
+
+ ::can_ok( __PACKAGE__, qw<get post> );
+}
+
+{
+ package App::GettingSelectiveDSL;
+ use Dancer2 '!post';
+
+ # proper way
+ ::can_ok( __PACKAGE__, 'get' );
+
+ # checking this would work too
+ ::ok( __PACKAGE__->can('get'), 'get imported successfully' );
+ ::ok( ! __PACKAGE__->can('post'), 'Can import keywords selectively' );
+}
+
+{
+ package App::Main;
+ use Dancer2;
+ get '/main' => sub {1};
+}
+
+{
+ package App::ComposedToMain;
+ use Dancer2 appname => 'App::Main';
+ get '/alsomain' => sub {1};
+}
+
+{
+ my $runner = Dancer2->runner;
+ isa_ok( $runner, 'Dancer2::Core::Runner' );
+ my $apps = $runner->apps;
+
+ cmp_ok(
+ scalar @{$apps},
+ '==',
+ 12,
+ 'Correct number of Apps created so far',
+ );
+
+ my @names = sort map +( $_->name ), @{$apps};
+
+ # this is the list of all Apps loaded in this test
+ is_deeply(
+ \@names,
+ [qw<
+ App::CreatingRunner
+ App::GettingDSL
+ App::GettingSelectiveDSL
+ App::KeyPairOnly
+ App::Main
+ App::NoStrictNoWarningsNoUTF8
+ App::NotRecreatingRunner
+ App::ScriptAllowed
+ App::StrictAndWarningsAndUTF8
+ App::SyntaxAllowed
+ App::TestsAllowed
+ App::WithSettingsChanged
+ >],
+ 'All apps accounted for',
+ );
+
+ my $app = App::Main->to_app;
+ isa_ok( $app, 'CODE' );
+ test_psgi $app, sub {
+ my $cb = shift;
+ is(
+ $cb->( GET '/main' )->content,
+ 1,
+ 'Got original app response',
+ );
+
+ is(
+ $cb->( GET '/alsomain' )->content,
+ 1,
+ 'Can compose apps with appname',
+ );
+ };
+}
+
+{
+ package App::WithSettingsChanged;
+ use Dancer2;
+}
+
+{
+ App::WithSettingsChanged->import( with => { layout => 'mobile' } );
+
+ my ($app) = grep +( $_->name eq 'App::WithSettingsChanged' ),
+ @{ Dancer2->runner->{'apps'} };
+
+ ::isa_ok( $app, 'Dancer2::Core::App' );
+ ::is(
+ $app->template_engine->{'layout'},
+ 'mobile',
+ 'Changed settings using with keyword',
+ );
+}
+
+{
+ package App::NoStrictNoWarningsNoUTF8;
+ use Dancer2;
+ no strict;
+ no warnings;
+ no utf8;
+
+ local $@ = undef;
+
+ eval '$var = 30';
+
+ ::is(
+ $@,
+ '',
+ 'no strict (control test)',
+ );
+
+ local $SIG{'__WARN__'} = sub {
+ ::is(
+ $_[0],
+ undef,
+ 'no warning (control test)',
+ );
+ };
+
+ eval 'my $var; my $var;';
+
+ my $str = "щука";
+ ::isnt( length $str, 4, 'utf8 pragma not imported' );
+}
+
+{
+ package App::StrictAndWarningsAndUTF8;
+ use Dancer2;
+
+ local $@ = undef;
+
+ local $SIG{'__WARN__'} = sub {
+ ::ok(
+ $_[0],
+ 'warnings pragma imported',
+ );
+ };
+
+ eval '$var = 30;';
+
+ ::like(
+ $@,
+ qr/^Global symbol/,
+ 'strict pragma imported',
+ );
+
+ eval 'my $var; my $var;';
+
+ my $str = "щука";
+ ::is( length $str, 4, 'utf8 pragma imported' );
+}
+
diff --git a/t/config.yml b/t/config.yml
new file mode 100644
index 00000000..33e39171
--- /dev/null
+++ b/t/config.yml
@@ -0,0 +1,6 @@
+log: "info"
+logger: "Note"
+
+plugins:
+ "FooPlugin":
+ plugin: 42
diff --git a/t/config/config.yml b/t/config/config.yml
new file mode 100644
index 00000000..032820ba
--- /dev/null
+++ b/t/config/config.yml
@@ -0,0 +1,6 @@
+main: 1
+charset: 'UTF-8'
+show_errors: 1
+
+application:
+ some_feature: foo
diff --git a/t/config/environments/failure.yml b/t/config/environments/failure.yml
new file mode 100644
index 00000000..ea4f9cf9
--- /dev/null
+++ b/t/config/environments/failure.yml
@@ -0,0 +1,3 @@
+not valid YAML - inconsistent indentation
+foo: 42
+ bar: baz
diff --git a/t/config/environments/merging.yml b/t/config/environments/merging.yml
new file mode 100644
index 00000000..ca45c18a
--- /dev/null
+++ b/t/config/environments/merging.yml
@@ -0,0 +1,3 @@
+application:
+ some_feature: bar
+ another_setting: baz
diff --git a/t/config/environments/production.yml b/t/config/environments/production.yml
new file mode 100644
index 00000000..116061c8
--- /dev/null
+++ b/t/config/environments/production.yml
@@ -0,0 +1,2 @@
+show_errors: 0
+logger: "console"
diff --git a/t/config/environments/staging.json b/t/config/environments/staging.json
new file mode 100644
index 00000000..2fdea433
--- /dev/null
+++ b/t/config/environments/staging.json
@@ -0,0 +1,5 @@
+{
+ "main": "1",
+ "charset": 'UTF-8',
+ "show_errors": "1"
+}
diff --git a/t/config2/config.yml b/t/config2/config.yml
new file mode 100644
index 00000000..14472e88
--- /dev/null
+++ b/t/config2/config.yml
@@ -0,0 +1,9 @@
+main: 1
+charset: 'UTF-8'
+show_errors: 1
+
+application:
+ feature_1: foo
+ feature_2: bar
+ feature_3: baz
+ feature_4: blat
diff --git a/t/config2/config_local.yml b/t/config2/config_local.yml
new file mode 100644
index 00000000..ce267c13
--- /dev/null
+++ b/t/config2/config_local.yml
@@ -0,0 +1,4 @@
+application:
+ feature_2: alpha
+ feature_5: beta
+ feature_6: gamma
diff --git a/t/config2/environments/lconfig.yml b/t/config2/environments/lconfig.yml
new file mode 100644
index 00000000..4f39f636
--- /dev/null
+++ b/t/config2/environments/lconfig.yml
@@ -0,0 +1,3 @@
+application:
+ feature_6: bar
+ feature_7: baz
diff --git a/t/config2/environments/lconfig_local.yml b/t/config2/environments/lconfig_local.yml
new file mode 100644
index 00000000..14f8fb5c
--- /dev/null
+++ b/t/config2/environments/lconfig_local.yml
@@ -0,0 +1,3 @@
+application:
+ feature_3: replacement
+ feature_8: goober
diff --git a/t/config_multiapp.t b/t/config_multiapp.t
new file mode 100644
index 00000000..a2acc6c4
--- /dev/null
+++ b/t/config_multiapp.t
@@ -0,0 +1,24 @@
+use strict;
+use warnings;
+
+use Test::More;
+use File::Spec;
+
+use lib '.';
+use t::app::t1::lib::App1;
+use t::app::t1::lib::Sub::App2;
+use t::app::t2::lib::App3;
+
+for my $app ( @{ Dancer2->runner->apps } ) {
+ # Need to determine path to config; use apps' name for now..
+ my $path = $app->name eq 'App3' ? 't2' : 't1';
+
+ is_deeply $app->config_files,
+ [ File::Spec->rel2abs(File::Spec->catfile( 't', 'app', $path, 'config.yml' )) ],
+ $app->name . ": config files found";
+
+ is $app->config->{app}->{config}, 'ok',
+ $app->name . ": config loaded properly"
+}
+
+done_testing;
diff --git a/t/config_reader.t b/t/config_reader.t
new file mode 100644
index 00000000..5dd4065e
--- /dev/null
+++ b/t/config_reader.t
@@ -0,0 +1,189 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Fatal;
+use Carp 'croak';
+
+use Dancer2::Core::Runner;
+use Dancer2::FileUtils qw/dirname path/;
+use File::Spec;
+use File::Temp;
+
+# undefine ENV vars used as defaults for app environment in these tests
+local $ENV{DANCER_ENVIRONMENT};
+local $ENV{PLACK_ENV};
+
+my $runner = Dancer2::Core::Runner->new();
+my $location = File::Spec->rel2abs( path( dirname(__FILE__), 'config' ) );
+my $location2 = File::Spec->rel2abs( path( dirname(__FILE__), 'config2' ) );
+
+{
+
+ package Prod;
+ use Moo;
+ with 'Dancer2::Core::Role::ConfigReader';
+
+ sub name {'Prod'}
+
+ sub _build_environment {'production'}
+ sub _build_location {$location}
+ sub _build_default_config {$runner->config}
+
+ package Dev;
+ use Moo;
+ with 'Dancer2::Core::Role::ConfigReader';
+
+ sub _build_environment {'development'}
+ sub _build_location {$location};
+ sub _build_default_config {$runner->config}
+
+ package Failure;
+ use Moo;
+ with 'Dancer2::Core::Role::ConfigReader';
+
+ sub _build_environment {'failure'}
+ sub _build_location {$location}
+ sub _build_default_config {$runner->config}
+
+ package Staging;
+ use Moo;
+ with 'Dancer2::Core::Role::ConfigReader';
+
+ sub _build_environment {'staging'}
+ sub _build_location {$location}
+ sub _build_default_config {$runner->config}
+
+ package Merging;
+ use Moo;
+ with 'Dancer2::Core::Role::ConfigReader';
+
+ sub name {'Merging'}
+
+ sub _build_environment {'merging'}
+ sub _build_location {$location}
+ sub _build_default_config {$runner->config}
+
+ package LocalConfig;
+ use Moo;
+ with 'Dancer2::Core::Role::ConfigReader';
+
+ sub name {'LocalConfig'}
+
+ sub _build_environment {'lconfig'}
+ sub _build_location {$location2}
+ sub _build_default_config {$runner->config}
+
+}
+
+my $d = Dev->new();
+is_deeply $d->config_files,
+ [ path( $location, 'config.yml' ), ],
+ "config_files() only sees existing files";
+
+my $f = Prod->new;
+is $f->does('Dancer2::Core::Role::ConfigReader'), 1,
+ "role Dancer2::Core::Role::ConfigReader is consumed";
+
+is_deeply $f->config_files,
+ [ path( $location, 'config.yml' ),
+ path( $location, 'environments', 'production.yml' ),
+ ],
+ "config_files() works";
+
+my $j = Staging->new;
+is_deeply $j->config_files,
+ [ path( $location, 'config.yml' ),
+ path( $location, 'environments', 'staging.json' ),
+ ],
+ "config_files() does JSON too!";
+
+note "bad YAML file";
+my $fail = Failure->new;
+is $fail->environment, 'failure';
+
+is_deeply $fail->config_files,
+ [ path( $location, 'config.yml' ),
+ path( $location, 'environments', 'failure.yml' ),
+ ],
+ "config_files() works";
+
+like(
+ exception { $fail->config },
+ qr{Unable to parse the configuration file}, 'Configuration file parsing failure',
+);
+
+note "config merging";
+my $m = Merging->new;
+
+# Check the 'application' top-level key; its the only key that
+# is currently a HoH in the test configurations
+is_deeply $m->config->{application},
+ { some_feature => 'bar',
+ another_setting => 'baz',
+ },
+ "full merging of configuration hashes";
+
+{
+ my $l = LocalConfig->new;
+
+ is_deeply $l->config_files,
+ [ path( $location2, 'config.yml' ),
+ path( $location2, 'config_local.yml' ),
+ path( $location2, 'environments', 'lconfig.yml' ),
+ path( $location2, 'environments', 'lconfig_local.yml' ),
+ ],
+ "config_files() with local config works";
+
+ is_deeply $l->config->{application},
+ { feature_1 => 'foo',
+ feature_2 => 'alpha',
+ feature_3 => 'replacement',
+ feature_4 => 'blat',
+ feature_5 => 'beta',
+ feature_6 => 'bar',
+ feature_7 => 'baz',
+ feature_8 => 'goober',
+ },
+ "full merging of local configuration hashes";
+
+}
+
+note "config parsing";
+
+is $f->config->{show_errors}, 0;
+is $f->config->{main}, 1;
+is $f->config->{charset}, 'utf-8', "normalized UTF-8 to utf-8";
+
+ok( $f->has_setting('charset') );
+ok( !$f->has_setting('foobarbaz') );
+
+note "default values";
+is $f->setting('apphandler'), 'Standalone';
+
+like(
+ exception { $f->_normalize_config( { charset => 'BOGUS' } ) },
+ qr{Charset defined in configuration is wrong : couldn't identify 'BOGUS'},
+ 'Configuration file charset failure',
+);
+
+{
+
+ package Foo;
+ use Carp 'croak';
+ sub foo { croak "foo" }
+}
+
+is $f->setting('traces'), 0;
+unlike( exception { Foo->foo() }, qr{Foo::foo}, "traces are not enabled", );
+
+$f->setting( traces => 1 );
+like( exception { Foo->foo() }, qr{Foo::foo}, "traces are enabled", );
+
+{
+ my $tmpdir = File::Temp::tempdir( CLEANUP => 1, TMPDIR => 1 );
+ $ENV{DANCER_CONFDIR} = $tmpdir;
+ my $f = Prod->new;
+ is $f->config_location, $tmpdir;
+}
+
+done_testing;
diff --git a/t/config_settings.t b/t/config_settings.t
new file mode 100644
index 00000000..d4df716f
--- /dev/null
+++ b/t/config_settings.t
@@ -0,0 +1,31 @@
+use 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;
diff --git a/t/context-in-before.t b/t/context-in-before.t
new file mode 100644
index 00000000..fd4c74e9
--- /dev/null
+++ b/t/context-in-before.t
@@ -0,0 +1,57 @@
+#!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' );
+};
+
+
diff --git a/t/cookie.t b/t/cookie.t
new file mode 100644
index 00000000..ac82b528
--- /dev/null
+++ b/t/cookie.t
@@ -0,0 +1,207 @@
+use strict;
+use warnings;
+use Test::Fatal;
+use Test::More;
+
+BEGIN {
+
+ # Freeze time at Tue, 15-Jun-2010 00:00:00 GMT
+ *CORE::GLOBAL::time = sub { return 1276560000 }
+}
+
+use Dancer2::Core::Cookie;
+use Dancer2::Core::Request;
+
+diag "If you want extra speed, install HTTP::XSCookies"
+ if !Dancer2::Core::Cookie::_USE_XS;
+
+sub run_test {
+
+ note "Constructor";
+
+ my $cookie = Dancer2::Core::Cookie->new( name => "foo" );
+
+ isa_ok $cookie => 'Dancer2::Core::Cookie';
+ can_ok $cookie => 'to_header';
+
+
+ note "Setting values";
+
+ is $cookie->value("foo") => "foo", "Can set value";
+ is $cookie->value => "foo", "Set value stuck";
+
+ is $cookie . "bar", "foobar", "Stringifies to desired value";
+
+ ok $cookie->value( [qw(a b c)] ), "can set multiple values";
+ is $cookie->value => 'a', "get first value in scalar context";
+ is_deeply [ $cookie->value ] => [qw(a b c)],
+ "get all values in list context";;
+
+ ok $cookie->value( { x => 1, y => 2 } ), "can set values with a hashref";
+ like $cookie->value => qr/^[xy]$/; # hashes doesn't store order...
+ is_deeply [ sort $cookie->value ] => [ sort ( 1, 2, 'x', 'y' ) ];
+
+
+ note "accessors and defaults";
+
+ is $cookie->name => 'foo', "name is as expected";
+ is $cookie->name("bar") => "bar", "can change name";
+ is $cookie->name => 'bar', "name change stuck";
+
+ ok !$cookie->domain, "no domain set by default";
+ is $cookie->domain("dancer.org") => "dancer.org",
+ "setting domain returns new value";
+ is $cookie->domain => "dancer.org",
+ "new domain valjue stuck";
+ is $cookie->domain("") => "", "can clear domain";
+ ok !$cookie->domain, "no domain set now";
+
+ is $cookie->path => '/', "by default, path is /";
+ ok $cookie->has_path, "has_path";
+ is $cookie->path("/foo") => "/foo", "setting path returns new value";
+ ok $cookie->has_path, "has_path";
+ is $cookie->path => "/foo", "new path stuck";
+
+ ok !$cookie->secure, "no cookie secure flag by default";
+ is $cookie->secure(1) => 1, "enabling \$cookie->secure returns new value";
+ is $cookie->secure => 1, "\$cookie->secure flag is enabled";
+ is $cookie->secure(0) => 0, "disabling \$cookie->secure returns new value";
+ ok !$cookie->secure, "\$cookie->secure flag is disabled";
+
+ ok $cookie->http_only, "http_only by default";
+ is $cookie->http_only(0) => 0,
+ "disabling \$cookie->http_only returns new value";
+ ok !$cookie->http_only,
+ "\$cookie->http_only is now disabled";
+
+ like exception { $cookie->same_site('foo') },
+ qr/Value "foo" did not pass type constraint "Enum\["Strict","Lax"\]/;
+
+ note "expiration strings";
+
+ my $min = 60;
+ my $hour = 60 * $min;
+ my $day = 24 * $hour;
+ my $week = 7 * $day;
+ my $mon = 30 * $day;
+ my $year = 365 * $day;
+
+ ok !$cookie->expires;
+ my %times = (
+ "+2" => "Tue, 15-Jun-2010 00:00:02 GMT",
+ "+2h" => "Tue, 15-Jun-2010 02:00:00 GMT",
+ "-2h" => "Mon, 14-Jun-2010 22:00:00 GMT",
+ "1 hour" => "Tue, 15-Jun-2010 01:00:00 GMT",
+ "3 weeks 4 days 2 hours 99 min 0 secs" => "Sat, 10-Jul-2010 03:39:00 GMT",
+ "2 months" => "Sat, 14-Aug-2010 00:00:00 GMT",
+ "12 years" => "Sun, 12-Jun-2022 00:00:00 GMT",
+
+ 1288817656 => "Wed, 03-Nov-2010 20:54:16 GMT",
+ 1288731256 => "Tue, 02-Nov-2010 20:54:16 GMT",
+ 1288644856 => "Mon, 01-Nov-2010 20:54:16 GMT",
+ 1288558456 => "Sun, 31-Oct-2010 20:54:16 GMT",
+ 1288472056 => "Sat, 30-Oct-2010 20:54:16 GMT",
+ 1288385656 => "Fri, 29-Oct-2010 20:54:16 GMT",
+ 1288299256 => "Thu, 28-Oct-2010 20:54:16 GMT",
+ 1288212856 => "Wed, 27-Oct-2010 20:54:16 GMT",
+
+ # Anything not understood is passed through
+ "basset hounds got long ears" => "basset hounds got long ears",
+ "+2 something" => "+2 something",
+ );
+
+ for my $exp ( keys %times ) {
+ my $want = $times{$exp};
+
+ $cookie->expires($exp);
+ is $cookie->expires => $want, "expiry $exp => $want";;
+ }
+
+
+ note "to header";
+
+ my @cake = (
+ { cookie => {
+ name => 'bar',
+ value => 'foo',
+ expires => '+2h',
+ secure => 1
+ },
+ expected => sprintf(
+ "bar=foo; Expires=%s; HttpOnly; Path=/; Secure",
+ $times{'+2h'},
+ ),
+ },
+ { cookie => {
+ name => 'bar',
+ value => 'foo',
+ domain => 'dancer.org',
+ path => '/dance',
+ http_only => 1
+ },
+ expected => "bar=foo; Domain=dancer.org; HttpOnly; Path=/dance",
+ },
+ { cookie => {
+ name => 'bar',
+ value => 'foo',
+ },
+ expected => "bar=foo; HttpOnly; Path=/",
+ },
+ { cookie => {
+ name => 'bar',
+ value => 'foo',
+ http_only => 0,
+ },
+ expected => "bar=foo; Path=/",
+ },
+ { cookie => {
+ name => 'bar',
+ value => 'foo',
+ http_only => '0',
+ },
+ expected => "bar=foo; Path=/",
+ },
+ { cookie => {
+ name => 'same-site',
+ value => 'strict',
+ same_site => 'Strict',
+ },
+ expected => 'same-site=strict; HttpOnly; Path=/; SameSite=Strict',
+ },
+ { cookie => {
+ name => 'same-site',
+ value => 'lax',
+ same_site => 'Lax',
+ },
+ expected => 'same-site=lax; HttpOnly; Path=/; SameSite=Lax',
+ },
+ );
+
+ for my $cook (@cake) {
+ my $c = Dancer2::Core::Cookie->new(%{$cook->{cookie}});
+ # name=value; sorted fields
+ my @a = split /; /, $c->to_header;
+ is join("; ", shift @a, sort @a), $cook->{expected};
+ }
+
+ note 'multi-value';
+
+ my $c = Dancer2::Core::Cookie->new( name => 'foo', value => [qw/bar baz/] );
+
+ is $c->to_header, 'foo=bar&baz; Path=/; HttpOnly';
+
+ my $r = Dancer2::Core::Request->new( env => { HTTP_COOKIE => 'foo=bar&baz' } );
+
+ is_deeply [ $r->cookies->{foo}->value ], [qw/bar baz/];
+}
+
+note "Run test with XS_HTTP_COOKIES" if Dancer2::Core::Cookie::_USE_XS;
+run_test();
+if ( Dancer2::Core::Cookie::_USE_XS ) {
+ note "Run test without XS_HTTP_COOKIES";
+ no warnings 'redefine';
+ *Dancer2::Core::Cookie::to_header = \&Dancer2::Core::Cookie::pp_to_header;
+ run_test();
+}
+
+done_testing;
diff --git a/t/corpus/pretty/505.tt b/t/corpus/pretty/505.tt
new file mode 100644
index 00000000..25c53d71
--- /dev/null
+++ b/t/corpus/pretty/505.tt
@@ -0,0 +1,4 @@
+Template selected.
+
+message: [% content %]
+status: [% status %]
diff --git a/t/corpus/pretty/relative.tt b/t/corpus/pretty/relative.tt
new file mode 100644
index 00000000..4deb0273
--- /dev/null
+++ b/t/corpus/pretty/relative.tt
@@ -0,0 +1,3 @@
+Template [% component.name %]
+
+[% INCLUDE 505.tt %]
diff --git a/t/corpus/pretty_public/404.html b/t/corpus/pretty_public/404.html
new file mode 100644
index 00000000..fe50ecd4
--- /dev/null
+++ b/t/corpus/pretty_public/404.html
@@ -0,0 +1 @@
+<html><body><h1>Yup, you're lost</h1></body></html>
diff --git a/t/corpus/pretty_public/510.html b/t/corpus/pretty_public/510.html
new file mode 100644
index 00000000..08e89711
--- /dev/null
+++ b/t/corpus/pretty_public/510.html
@@ -0,0 +1 @@
+Static page.
diff --git a/t/corpus/static/1x1.png b/t/corpus/static/1x1.png
new file mode 100644
index 00000000..1914264c
--- /dev/null
+++ b/t/corpus/static/1x1.png
Binary files differ
diff --git a/t/corpus/static/index.html b/t/corpus/static/index.html
new file mode 100644
index 00000000..2da8ed9d
--- /dev/null
+++ b/t/corpus/static/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Unicode test</title>
+ </head>
+ <body>
+ <h1>Hello, UTF-8</h1>
+ <p>áéíóú</p>
+ </body>
+</html>
diff --git a/t/custom_dsl.t b/t/custom_dsl.t
new file mode 100644
index 00000000..c82a5fcb
--- /dev/null
+++ b/t/custom_dsl.t
@@ -0,0 +1,30 @@
+use 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();
diff --git a/t/dancer-test.t b/t/dancer-test.t
new file mode 100644
index 00000000..fdece388
--- /dev/null
+++ b/t/dancer-test.t
@@ -0,0 +1,153 @@
+# who test the tester? We do!
+
+use strict;
+use warnings;
+use File::Spec;
+use File::Basename qw/dirname/;
+use Ref::Util qw<is_arrayref>;
+
+BEGIN {
+ # Disable route handlers so we can actually test route_exists
+ # and route_doesnt_exist. Use config that disables default route handlers.
+ $ENV{DANCER_CONFDIR} = File::Spec->catdir(dirname(__FILE__), 'dancer-test');
+}
+
+use Test::More tests => 50;
+
+use Dancer2;
+use Dancer2::Test;
+use Dancer2::Core::Request;
+use File::Temp;
+use Encode;
+use URI::Escape;
+
+$Dancer2::Test::NO_WARN = 1;
+
+my @routes = (
+ '/foo',
+ [ GET => '/foo' ],
+ Dancer2::Core::Request->new(
+ env => {
+ 'psgi.url_scheme' => 'http',
+ REQUEST_METHOD => 'GET',
+ QUERY_STRING => '',
+ SERVER_NAME => 'localhost',
+ SERVER_PORT => 5000,
+ SERVER_PROTOCOL => 'HTTP/1.1',
+ SCRIPT_NAME => '',
+ PATH_INFO => '/foo',
+ REQUEST_URI => '/foo',
+ }
+ ),
+);
+my $fighter = Dancer2::Core::Response->new(
+ content => 'fighter',
+ status => 404,
+);
+
+route_doesnt_exist $_ for (@routes, $fighter);
+
+
+get '/foo' => sub {'fighter'};
+
+route_exists $_, "route $_ exists" for @routes;
+
+$fighter->status(200);
+push @routes, $fighter;
+
+for (@routes) {
+ my $response = dancer_response $_;
+ isa_ok $response => 'Dancer2::Core::Response';
+ is $response->content => 'fighter';
+}
+
+response_content_is $_ => 'fighter', "response_content_is with $_" for @routes;
+response_content_isnt $_ => 'platypus', "response_content_isnt with $_"
+ for @routes;
+response_content_like $_ => qr/igh/ for @routes;
+response_content_unlike $_ => qr/ought/ for @routes;
+
+response_status_is $_ => 200 for @routes;
+response_status_isnt $_ => 203 for @routes;
+
+response_headers_include $_ => [ Server => "Perl Dancer2 " . Dancer2->VERSION ]
+ for @routes;
+
+## Check parameters get through ok
+get '/param' => sub { param('test') };
+my $param_response =
+ dancer_response( GET => '/param', { params => { test => 'hello' } } );
+is $param_response->content, 'hello', 'PARAMS get echoed by route';
+
+post '/upload' => sub {
+ my $file = upload('test');
+ return $file->content;
+};
+## Check we can upload files
+my $file_response = dancer_response(
+ POST => '/upload',
+ { files =>
+ [ { filename => 'test.txt', name => 'test', data => 'testdata' } ]
+ }
+);
+is $file_response->content, 'testdata', 'file uploaded with supplied data';
+
+my $temp = File::Temp->new;
+print $temp 'testfile';
+close($temp);
+
+$file_response =
+ dancer_response( POST => '/upload',
+ { files => [ { filename => $temp->filename, name => 'test' } ] } );
+is $file_response->content, 'testfile', 'file uploaded with supplied filename';
+
+## Check multiselect/multi parameters get through ok
+get '/multi' => sub {
+ my $t = param('test');
+ return 'bad' if !is_arrayref($t);
+ my $p = join( '', @$t );
+ return $p;
+};
+$param_response =
+ dancer_response( GET => '/multi',
+ { params => { test => [ 'foo', 'bar' ] } } );
+is $param_response->content, 'foobar',
+ 'multi values for same key get echoed back';
+
+my $russian_test =
+ decode( 'UTF-8',
+ uri_unescape("%D0%B8%D1%81%D0%BF%D1%8B%D1%82%D0%B0%D0%BD%D0%B8%D0%B5") );
+$param_response =
+ dancer_response( GET => '/multi',
+ { params => { test => [ 'test/', $russian_test ] } } );
+is $param_response->content, 'test/' . encode( 'UTF-8', $russian_test ),
+ 'multi utf8 value properly merge';
+
+get '/headers' => sub {
+ join " : ", request->header('X-Sent-By'), request->cookies->{foo};
+};
+note "extra headers in request";
+sub extra_headers {
+ my $sent_by = 'Dancer2::Test';
+ my $headers_test = dancer_response( GET => '/headers',
+ {
+ headers => [
+ [ 'X-Sent-By' => $sent_by ],
+ [ 'Cookie' => "foo=bar" ],
+ ],
+ }
+ );
+ is $headers_test->content, "$sent_by : bar",
+ "extra headers included in request";
+}
+
+note "Run extra_headers test with XS_HTTP_COOKIES"
+ if $Dancer2::Core::Request::XS_HTTP_COOKIES;
+extra_headers();
+SKIP: {
+ skip "HTTP::XSCookies not installed", 1
+ if !$Dancer2::Core::Request::XS_HTTP_COOKIES;
+ note "Run extra_headers test without XS_HTTP_COOKIES";
+ $Dancer2::Core::Request::XS_HTTP_COOKIES = 0;
+ extra_headers();
+}
diff --git a/t/dancer-test/config.yml b/t/dancer-test/config.yml
new file mode 100644
index 00000000..75d8d745
--- /dev/null
+++ b/t/dancer-test/config.yml
@@ -0,0 +1 @@
+route_handlers: []
diff --git a/t/deserialize.t b/t/deserialize.t
new file mode 100644
index 00000000..4ed051bd
--- /dev/null
+++ b/t/deserialize.t
@@ -0,0 +1,215 @@
+use strict;
+use warnings;
+
+use Test::More tests => 15;
+use Plack::Test;
+use HTTP::Request::Common;
+use Dancer2::Logger::Capture;
+
+my $logger = Dancer2::Logger::Capture->new;
+isa_ok( $logger, 'Dancer2::Logger::Capture' );
+
+{
+ package App;
+ use Dancer2;
+
+ # default, we're actually overriding this later
+ set serializer => 'JSON';
+
+ # for now
+ set logger => 'Console';
+
+ put '/from_params' => sub {
+ my %p = params();
+ return [ map +( $_ => $p{$_} ), sort keys %p ];
+ };
+
+ put '/from_data' => sub {
+ my $p = request->data;
+ return [ map +( $_ => $p->{$_} ), sort keys %{$p} ];
+ };
+
+ # This route is used for both toure and body params.
+ post '/from/:town' => sub {
+ my $p = params;
+ return [ map +( $_ => $p->{$_} ), sort keys %{$p} ];
+ };
+
+ any [qw/del patch/] => '/from/:town' => sub {
+ my $p = params('body');
+ return [ map +( $_ => $p->{$_} ), sort keys %{$p} ];
+ };
+}
+
+my $test = Plack::Test->create( App->to_app );
+
+subtest 'PUT request with parameters' => sub {
+ for my $type ( qw<params data> ) {
+ my $res = $test->request(
+ PUT "/from_$type",
+ 'Content-Type' => 'application/json',
+ Content => '{ "foo": 1, "bar": 2 }'
+ );
+
+ is(
+ $res->content,
+ '["bar",2,"foo",1]',
+ "Parameters deserialized from $type",
+ );
+ }
+};
+
+my $app = App->to_app;
+use utf8;
+use JSON::MaybeXS;
+use Encode;
+use Module::Runtime 'use_module';
+
+note "Verify Serializers decode into characters"; {
+ my $utf8 = '∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i)';
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ for my $type ( qw/Dumper JSON YAML/ ) {
+ my $class = "Dancer2::Serializer::$type";
+ use_module($class);
+
+ my $serializer = $class->new();
+ my $body = $serializer->serialize({utf8 => $utf8});
+
+ # change the app serializer
+ # we're overiding a RO attribute only for this test!
+ Dancer2->runner->apps->[0]->set_serializer_engine(
+ $serializer
+ );
+
+ my $r = $cb->(
+ PUT '/from_params',
+ 'Content-Type' => $serializer->content_type,
+ Content => $body,
+ );
+
+ my $content = Encode::decode( 'UTF-8', $r->content );
+
+ # Dumper is a jerk and represents it in Perl \x{...} notation
+
+ if ( $type eq 'Dumper' ) {
+ {
+ no strict;
+ $content = eval $content;
+ }
+
+ # now $content is an actual ref again
+ is_deeply(
+ $content,
+ [ 'utf8', $utf8 ],
+ "utf-8 string returns the same using the $type serializer",
+ )
+ } else {
+ like(
+ $content,
+ qr{\Q$utf8\E},
+ "utf-8 string returns the same using the $type serializer",
+ );
+ }
+ }
+ };
+}
+
+# default back to JSON for the rest
+# we're overiding a RO attribute only for this test!
+Dancer2->runner->apps->[0]->set_serializer_engine(
+ Dancer2::Serializer::JSON->new
+);
+
+note "Decoding of mixed route and deserialized body params"; {
+ # Check integers from request body remain integers
+ # but route params get decoded.
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ my @req_params = (
+ "/from/D\x{c3}\x{bc}sseldorf", # /from/d%C3%BCsseldorf
+ 'Content-Type' => 'application/json',
+ Content => JSON::MaybeXS::encode_json({ population => 592393 }),
+ );
+
+ my $r = $cb->( POST @req_params );
+
+ # Watch out for hash order randomization..
+ is_deeply(
+ $r->content,
+ '["population",592393,"town","'."D\x{c3}\x{bc}sseldorf".'"]',
+ "Integer from JSON body remains integer and route params decoded",
+ );
+ };
+}
+
+# Check body is deserialized on PATCH and DELETE.
+# The RFC states the behaviour for DELETE is undefined; We take the lenient
+# and deserialize it.
+# http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-24#section-4.3.5
+note "Deserialze any body content that is allowed or undefined"; {
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ for my $method ( qw/DELETE PATCH/ ) {
+ my $request = HTTP::Request->new(
+ $method,
+ "/from/D\x{c3}\x{bc}sseldorf", # /from/d%C3%BCsseldorf
+ [ 'Content-Type' => 'application/json' ],
+ JSON::MaybeXS::encode_json({ population => 592393 }),
+ );
+ my $response = $cb->($request);
+ my $content = Encode::decode( 'UTF-8', $response->content );
+
+ # Only body params returned
+ is(
+ $content,
+ '["population",592393]',
+ "JSON body deserialized for " . uc($method) . " requests",
+ );
+ }
+ }
+}
+
+note 'Check serialization errors'; {
+ Dancer2->runner->apps->[0]->set_serializer_engine(
+ Dancer2::Serializer::JSON->new( log_cb => sub { $logger->log(@_) } )
+ );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ $cb->(
+ PUT '/from_params',
+ 'Content-Type' => 'application/json',
+ Content => '---',
+ );
+
+ my $trap = $logger->trapper;
+ isa_ok( $trap, 'Dancer2::Logger::Capture::Trap' );
+
+ my $errors = $trap->read;
+ isa_ok( $errors, 'ARRAY' );
+ is( scalar @{$errors}, 1, 'One error caught' );
+
+ my $msg = $errors->[0];
+ delete $msg->{'formatted'};
+ isa_ok( $msg, 'HASH' );
+ is( scalar keys %{$msg}, 2, 'Two items in the error' );
+
+ is( $msg->{'level'}, 'core', 'Correct level' );
+ like(
+ $msg->{'message'},
+ qr{
+ ^
+ \QFailed to deserialize content: \E
+ \Qmalformed number\E
+ }x,
+ 'Correct error message',
+ );
+ }
+}
+
diff --git a/t/disp_named_capture.t b/t/disp_named_capture.t
new file mode 100644
index 00000000..1fdf4048
--- /dev/null
+++ b/t/disp_named_capture.t
@@ -0,0 +1,35 @@
+use 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 $+
+# eval'd as named captures are not available until 5.10
+my $c;
+eval <<'NAMED';
+"12345" =~ m#(?<capture>23)#;
+$c = $+{capture};
+NAMED
+
+$request = HTTP::Request->new( GET => 'http://localhost/2' );
+$response = $test->request( $request );
+is( $response->content, 2 );
+
diff --git a/t/dispatcher.t b/t/dispatcher.t
new file mode 100644
index 00000000..cbda6ad3
--- /dev/null
+++ b/t/dispatcher.t
@@ -0,0 +1,251 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'];
+use Carp 'croak';
+use Ref::Util qw<is_regexpref>;
+
+use Dancer2;
+use Dancer2::Core::App;
+use Dancer2::Core::Route;
+use Dancer2::Core::Dispatcher;
+use Dancer2::Core::Hook;
+use Dancer2::Core::Response;
+
+set logger => 'Null';
+
+# init our test fixture
+my $buffer = {};
+my $app = Dancer2::Core::App->new( name => 'main' );
+
+$app->setting( logger => engine('logger') );
+$app->setting( show_errors => 1 );
+
+# a simple / route
+my $simple_route = $app->add_route(
+ method => 'get',
+ regexp => '/',
+ code => sub {"home"},
+);
+
+# an error route
+my $error_route = $app->add_route(
+ method => 'get',
+ regexp => '/error',
+ code => sub { Fail->fail },
+);
+
+# A chain of two route for /user/$foo
+my $user_name_route = $app->add_route(
+ method => 'get',
+ regexp => '/user/:name',
+ code => sub {
+ my $app = shift;
+ $buffer->{user} = $app->request->params->{'name'};
+ $app->response->has_passed(1);
+ },
+);
+
+my $user_splat_route = $app->add_route(
+ method => 'get',
+ regexp => '/user/*?',
+ code => sub {
+ my $app = shift;
+ "Hello " . $app->request->params->{'name'};
+ },
+);
+
+# a route with a 204 response
+my $removed_content_route = $app->add_route(
+ method => 'get',
+ regexp => '/twoohfour',
+ code => sub {
+ my $app = shift;
+ $app->response->status(204);
+ "This content should be removed";
+ },
+);
+
+my $route_from_request;
+
+# simulates a redirect with halt
+$app->add_hook(
+ Dancer2::Core::Hook->new(
+ name => 'before',
+ code => sub {
+ my $app = shift;
+ $route_from_request = $app->request->route;
+ if ( $app->request->path_info eq '/haltme' ) {
+ $app->response->header( Location => 'http://perldancer.org' );
+ $app->response->status(302);
+ $app->response->is_halted(1);
+ }
+ },
+ )
+);
+
+my $was_in_second_filter = 0;
+$app->add_hook(
+ Dancer2::Core::Hook->new(
+ name => 'before',
+ code => sub {
+ my $app = shift;
+ if ( $app->request->path_info eq '/haltme' ) {
+ $was_in_second_filter =
+ 1; # should not happen because first filter halted the flow
+ }
+ },
+ )
+);
+
+my $halt_route = $app->add_route(
+ method => 'get',
+ regexp => '/haltme',
+ code => sub {"should not get there"},
+);
+# the tests
+my @tests = (
+ { env => {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => '/',
+ },
+ expected => [
+ 200,
+ [ 'Content-Length' => 4,
+ 'Content-Type' => 'text/html; charset=UTF-8',
+ 'Server' => "Perl Dancer2 " . Dancer2->VERSION,
+ ],
+ ["home"],
+ $simple_route,
+ ]
+ },
+ { env => {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => '/user/Johnny',
+ },
+ expected => [
+ 200,
+ [ 'Content-Length' => 12,
+ 'Content-Type' => 'text/html; charset=UTF-8',
+ 'Server' => "Perl Dancer2 " . Dancer2->VERSION,
+ ],
+ ["Hello Johnny"],
+ $user_splat_route, # the second, after the first pass()es
+ ]
+ },
+ { env => {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => '/twoohfour',
+ },
+ expected => [
+ 204,
+ [ 'Content-Type' => 'text/html; charset=UTF-8',
+ 'Server' => "Perl Dancer2 " . Dancer2->VERSION,
+ ],
+ [],
+ $removed_content_route,
+ ]
+ },
+ { env => {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => '/haltme',
+ },
+ expected => [
+ 302,
+ [ 'Location' => 'http://perldancer.org',
+ 'Content-Length' => '305',
+ 'Content-Type' => 'text/html; charset=utf-8',
+ 'Server' => "Perl Dancer2 " . Dancer2->VERSION,
+ ],
+ qr/This item has moved/,
+ $halt_route,
+ ]
+ },
+
+# NOT SUPPORTED YET
+# { env => {
+# REQUEST_METHOD => 'GET',
+# PATH_INFO => '/admin',
+# },
+# expected => [200, [], ["home"]]
+# },
+
+
+);
+
+$app->compile_hooks;
+
+plan tests => 20;
+
+my $dispatcher = Dancer2::Core::Dispatcher->new( apps => [$app] );
+my $counter = 0;
+foreach my $test (@tests) {
+ my $env = $test->{env};
+ my $expected = $test->{expected};
+ my $path = $env->{'PATH_INFO'};
+
+ $route_from_request = undef;
+
+ diag sprintf "Dispatch test %d, for %s %s",
+ $counter,
+ $test->{env}{REQUEST_METHOD},
+ $test->{env}{PATH_INFO};
+
+ my $resp = $dispatcher->dispatch($env);
+
+ is( $resp->[0], $expected->[0], "[$path] Return code ok" );
+
+ my %got_headers = @{ $resp->[1] };
+ my %exp_headers = @{ $expected->[1] };
+ is_deeply( \%got_headers, \%exp_headers, "[$path] Correct headers" );
+
+ if ( is_regexpref( $expected->[2] ) ) {
+ like $resp->[2][0] => $expected->[2], "[$path] Contents ok. (test $counter)";
+ }
+ else {
+ is_deeply $resp->[2] => $expected->[2], "[$path] Contents ok. (test $counter)";
+ }
+
+
+ is(
+ $route_from_request, # squirreled away by before hook,
+ $expected->[3],
+ "Expected route is stored in request (test $counter)",
+ );
+
+ $counter++;
+}
+
+foreach my $test (
+ { env => {
+ REQUEST_METHOD => 'GET',
+ PATH_INFO => '/error',
+ 'psgi.uri_scheme' => 'http',
+ SERVER_NAME => 'localhost',
+ SERVER_PORT => 5000,
+ SERVER_PROTOCOL => 'HTTP/1.1',
+ },
+ expected => [
+ 500,
+ [ 'Content-Length', "Content-Type", 'text/html' ],
+ qr!Internal Server Error.*Can&#39;t locate object method &quot;fail&quot; via package &quot;Fail&quot; \(perhaps you forgot to load &quot;Fail&quot;\?\) 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";
diff --git a/t/dsl/any.t b/t/dsl/any.t
new file mode 100644
index 00000000..3954e603
--- /dev/null
+++ b/t/dsl/any.t
@@ -0,0 +1,48 @@
+use 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<GET POST>;
+ my @fails = qw<PUT DELETE OPTIONS PATCH NONEXIST>;
+
+ 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<GET POST PUT DELETE OPTIONS PATCH> ) {
+ my $req = HTTP::Request->new( $method => '/all' );
+ is(
+ $test->request($req)->content,
+ $method,
+ "Method $method works",
+ );
+ }
+};
+
diff --git a/t/dsl/app.t b/t/dsl/app.t
new file mode 100644
index 00000000..9f891888
--- /dev/null
+++ b/t/dsl/app.t
@@ -0,0 +1,18 @@
+use 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 '/' );
+
diff --git a/t/dsl/content.t b/t/dsl/content.t
new file mode 100644
index 00000000..68e5f5d1
--- /dev/null
+++ b/t/dsl/content.t
@@ -0,0 +1,49 @@
+use strict;
+use warnings;
+use Test::More tests => 1;
+use Plack::Test;
+use HTTP::Request::Common;
+
+my $logger;
+{
+ package App::ContentFail; ## no critic
+ use Dancer2;
+ set show_errors => 1;
+ set logger => 'Capture';
+
+ $logger = app->engine('logger');
+
+ get '/' => sub { content 'Foo' };
+}
+
+subtest 'content keyword can only be used within delayed response' => sub {
+ my $test = Plack::Test->create( App::ContentFail->to_app );
+ my $res = $test->request( GET '/' );
+ ok( ! $res->is_success, 'Request failed' );
+ is( $res->code, 500, 'Correct response code' );
+ like(
+ $res->content,
+ qr/Cannot use content keyword outside delayed response/,
+ 'Failed to use content keyword outside delayed response',
+ );
+
+ isa_ok( $logger, 'Dancer2::Logger::Capture' );
+ my $trapper = $logger->trapper;
+ isa_ok( $trapper, 'Dancer2::Logger::Capture::Trap' );
+
+ my $error = $trapper->read;
+ isa_ok( $error, 'ARRAY' );
+ is( scalar @{$error}, 1, 'Only one error' );
+ ok( delete $error->[0]{'formatted'}, 'Got formatted message' );
+ like(
+ delete $error->[0]{'message'},
+ qr{^\QRoute exception: Cannot use content keyword outside delayed response\E},
+ 'Correct error message',
+ );
+
+ is_deeply(
+ $error,
+ [ { level => 'error' } ],
+ 'Rest of error okay',
+ );
+};
diff --git a/t/dsl/delayed.t b/t/dsl/delayed.t
new file mode 100644
index 00000000..8ad26ee9
--- /dev/null
+++ b/t/dsl/delayed.t
@@ -0,0 +1,161 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+
+eval { require AnyEvent; 1; }
+ or plan skip_all => 'AnyEvent required for this test';
+
+plan tests => 5;
+
+{
+ package App::Content; ## no critic
+ use Dancer2;
+ get '/' => sub {
+ ::is( $Dancer2::Core::Route::RESPONDER, undef, 'No responder yet' );
+
+ delayed {
+ ::isa_ok(
+ $Dancer2::Core::Route::RESPONDER,
+ 'CODE',
+ 'Got a responder in the delayed callback',
+ );
+
+ ::is( $Dancer2::Core::Route::WRITER, undef, 'No writer yet' );
+
+ content 'OK';
+ ::ok( $Dancer2::Core::Route::WRITER, 'Got a writer' );
+
+ done;
+ };
+ };
+}
+
+{
+ package App::Content::MultiWrite; ## no critic
+ use Dancer2;
+ get '/' => sub {
+ delayed {
+ flush;
+ content 'Foo';
+ content 'Bar';
+ done;
+ };
+ };
+}
+
+{
+ package App::NoContent; ## no critic
+ use Dancer2;
+ get '/' => sub {
+ delayed {content;done;'Not OK'};
+ };
+}
+
+{
+ package App::MultipleContent; ## no critic
+ use Dancer2;
+ get '/' => sub {
+ delayed {
+ content 'Bar';
+ done;
+ };
+ return 'OK';
+ };
+}
+
+my $caught_error;
+{
+ package App::ErrorHandler; ## no critic
+ use Dancer2;
+ require AnyEvent;
+ set logger => 'Capture';
+ get '/log' => sub {
+ delayed {
+ flush;
+ content "ping\n";
+ done;
+ content "failure\n";
+ };
+ };
+
+ get '/cb' => sub {
+ delayed {
+ flush;
+ content "ping\n";
+ done;
+ content "failure\n";
+ } on_error => sub {
+ $caught_error = shift;
+ };
+ };
+}
+
+subtest 'Testing an app with content keyword' => sub {
+ my $test = Plack::Test->create( App::Content->to_app );
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content, 'OK', 'Correct content' );
+};
+
+subtest 'Testing an app with multiple content keyword calls' => sub {
+ my $test = Plack::Test->create( App::Content::MultiWrite->to_app );
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content, 'FooBar', 'Correct content' );
+};
+
+subtest 'Testing an app without content keyword' => sub {
+ my $test = Plack::Test->create( App::NoContent->to_app );
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content, '', 'Correct content' );
+};
+
+subtest 'Delayed response ignored for non-delayed content' => sub {
+ my $test = Plack::Test->create( App::MultipleContent->to_app );
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content, 'OK', 'Correct content' );
+};
+
+subtest 'Delayed response error handling' => sub {
+ my $test = Plack::Test->create( App::ErrorHandler->to_app );
+
+ TODO: {
+ local $TODO = 'Does not work in development server';
+
+ my $res = $test->request( GET '/log' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content, "ping\n", 'Correct content' );
+
+ my $logger = App::ErrorHandler::app->logger_engine;
+ my $logs = $logger->trapper->read;
+ isa_ok( $logs, 'ARRAY', 'Got logs' );
+ is( scalar @{$logs}, 1, 'Got a message' );
+
+ my $msg = shift @{$logs};
+ ok( $msg, 'Got message' );
+ isa_ok( $msg, 'HASH', 'Got message' );
+ is(
+ $msg->{'level'},
+ 'core',
+ 'Correct error message level',
+ );
+
+ like(
+ $msg->{'message'},
+ qr/^Error in delayed response:/,
+ 'Got error',
+ );
+ }
+
+ TODO: {
+ local $TODO = 'Does not work in development server';
+ my $res = $test->request( GET '/cb' );
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content, "ping\n", 'Correct content' );
+ like( $caught_error, qr/^Error in delayed response:/, 'Got error' );
+ }
+};
diff --git a/t/dsl/error_template.t b/t/dsl/error_template.t
new file mode 100644
index 00000000..c583b518
--- /dev/null
+++ b/t/dsl/error_template.t
@@ -0,0 +1,78 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+{
+ 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_errors => 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{<h1>Error 404 - Not Found</h1>},
+ 'Error message looks good';
+ unlike $res->content, qr{Stack}, 'Error contains no stack trace';
+};
+
+done_testing;
diff --git a/t/dsl/extend.t b/t/dsl/extend.t
new file mode 100644
index 00000000..abbb3178
--- /dev/null
+++ b/t/dsl/extend.t
@@ -0,0 +1,53 @@
+# define a sample DSL extension that will be used in the rest of these test
+# This extends Dancer2::Core::DSL but provides an extra keyword
+#
+# Each test below creates a new package so it can load Dancer2
+BEGIN {
+
+ package Dancer2::Test::ExtendedDSL;
+
+ use Moo;
+ extends 'Dancer2::Core::DSL';
+
+ sub BUILD {
+ my ( $self ) = @_;
+ $self->register(foo => 1);
+ }
+
+ sub foo {
+ return $_[1];
+ }
+}
+
+package main;
+
+use Test::More tests => 5;
+
+package test1;
+use Test::More;
+
+use Dancer2 dsl => 'Dancer2::Test::ExtendedDSL';
+
+ok(defined &foo, 'use line dsl can foo');
+is(foo('bar'), 'bar', 'use line Foo returns bar');
+
+package test2;
+use Test::More;
+
+ok(!defined &foo, 'intermediate package has no polluted namespace');
+
+package test3;
+use Test::More;
+use FindBin;
+use File::Spec;
+
+BEGIN {
+ $ENV{DANCER_CONFDIR} = File::Spec->catdir($FindBin::Bin, 'extend_config');
+}
+
+use Dancer2;
+
+ok(defined &foo, 'config specified DSL can foo');
+is(foo('baz'), 'baz', 'config specified Foo returns baz');
+
+done_testing;
diff --git a/t/dsl/extend_config/config.yml b/t/dsl/extend_config/config.yml
new file mode 100644
index 00000000..da1f799b
--- /dev/null
+++ b/t/dsl/extend_config/config.yml
@@ -0,0 +1 @@
+dsl_class: 'Dancer2::Test::ExtendedDSL'
diff --git a/t/dsl/halt.t b/t/dsl/halt.t
new file mode 100644
index 00000000..1e9e3ca8
--- /dev/null
+++ b/t/dsl/halt.t
@@ -0,0 +1,86 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+subtest 'halt within routes' => sub {
+ {
+
+ package App;
+ use Dancer2;
+
+ get '/' => sub { 'hello' };
+ get '/halt' => sub {
+ response_header 'X-Foo' => 'foo';
+ halt;
+ };
+ get '/shortcircuit' => sub {
+ app->response->content('halted');
+ halt;
+ redirect '/'; # won't get executed as halt returns immediately.
+ };
+ }
+
+ my $app = App->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ {
+ my $res = $cb->( GET '/shortcircuit' );
+ is( $res->code, 200, '[/shortcircuit] Correct status' );
+ is( $res->content, 'halted', '[/shortcircuit] Correct content' );
+
+ }
+
+ {
+ my $res = $cb->( GET '/halt' );
+
+ is(
+ $res->server,
+ "Perl Dancer2 " . Dancer2->VERSION,
+ '[/halt] Correct Server header',
+ );
+
+ is(
+ $res->headers->header('X-Foo'),
+ 'foo',
+ '[/halt] Correct X-Foo header',
+ );
+ }
+ };
+
+};
+
+subtest 'halt in before hook' => sub {
+ {
+ package App;
+ use Dancer2;
+
+ hook before => sub {
+ response->content('I was halted');
+ halt if request->path eq '/shortcircuit';
+ };
+
+ }
+
+ my $app = App->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+ my $res = $cb->( GET '/shortcircuit' );
+
+ is( $res->code, 200, '[/shortcircuit] Correct code with before hook' );
+ is(
+ $res->content,
+ 'I was halted',
+ '[/shortcircuit] Correct content with before hook',
+ );
+ };
+};
+
+done_testing;
diff --git a/t/dsl/halt_with_param.t b/t/dsl/halt_with_param.t
new file mode 100644
index 00000000..ba9e4d3e
--- /dev/null
+++ b/t/dsl/halt_with_param.t
@@ -0,0 +1,85 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+subtest 'halt with parameter within routes' => sub {
+ {
+
+ package App;
+ use Dancer2;
+
+ get '/' => sub { 'hello' };
+ get '/halt' => sub {
+ response_header 'X-Foo' => 'foo';
+ halt;
+ };
+ get '/shortcircuit' => sub {
+ halt('halted');
+ redirect '/'; # won't get executed as halt returns immediately.
+ };
+ }
+
+ my $app = App->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ {
+ my $res = $cb->( GET '/shortcircuit' );
+ is( $res->code, 200, '[/shortcircuit] Correct status' );
+ is( $res->content, 'halted', '[/shortcircuit] Correct content' );
+
+ }
+
+ {
+ my $res = $cb->( GET '/halt' );
+
+ is(
+ $res->server,
+ "Perl Dancer2 " . Dancer2->VERSION,
+ '[/halt] Correct Server header',
+ );
+
+ is(
+ $res->headers->header('X-Foo'),
+ 'foo',
+ '[/halt] Correct X-Foo header',
+ );
+ }
+ };
+
+};
+
+subtest 'halt with parameter in before hook' => sub {
+ {
+ package App;
+ use Dancer2;
+
+ hook before => sub {
+ halt('I was halted') 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;
+
diff --git a/t/dsl/json.t b/t/dsl/json.t
new file mode 100644
index 00000000..9f62e6b2
--- /dev/null
+++ b/t/dsl/json.t
@@ -0,0 +1,20 @@
+use 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 '/' );
diff --git a/t/dsl/parameters.t b/t/dsl/parameters.t
new file mode 100644
index 00000000..491071ff
--- /dev/null
+++ b/t/dsl/parameters.t
@@ -0,0 +1,396 @@
+use 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<foo bar> ],
+ '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<foo bar baz>, [ qw<quux quuks> ] ],
+ 'Got splat values',
+ );
+ };
+
+ # /foo/bar/baz
+ get '/*/:foo/**' => sub {
+ my $params = route_parameters;
+ ::isa_ok(
+ $params,
+ 'Hash::MultiValue',
+ 'parameters keyword returns Hash::MultiValue object',
+ );
+
+ ::is( $params->get('foo'), 'bar', 'Correct route parameter' );
+
+ ::is_deeply(
+ [ splat ],
+ [ 'foo', ['baz', ''] ],
+ 'Got splat values',
+ );
+ };
+ }
+
+ my $app = Plack::Test->create( App::Route::Splat->to_app );
+
+ {
+ my $res = $app->request( GET '/foo' );
+ ok( $res->is_success, 'Successful request' );
+ }
+
+ {
+ my $res = $app->request( GET '/foo/bar' );
+ ok( $res->is_success, 'Successful request' );
+ }
+
+ {
+ my $res = $app->request( GET '/foo/bar/baz/quux/quuks' );
+ ok( $res->is_success, 'Successful request' );
+ }
+
+ {
+ my $res = $app->request( GET '/foo/bar/baz/' );
+ ok( $res->is_success, 'Successful request' );
+ }
+};
+
+subtest 'Captured route parameters' => sub {
+ {
+ package App::Route::Capture; ## no critic
+ use Dancer2;
+ get qr{^/foo/([^/]+)$} => sub {
+ my $params = route_parameters;
+ ::isa_ok(
+ $params,
+ 'Hash::MultiValue',
+ 'parameters keyword',
+ );
+
+ ::is_deeply(
+ { %{$params} },
+ {},
+ 'All route parameters are empty',
+ );
+
+ ::is_deeply(
+ [ splat ],
+ ['bar'],
+ 'Correct splat values',
+ );
+
+ ::is_deeply(
+ captures(),
+ +{},
+ 'capture values are empty',
+ );
+ };
+ }
+
+ my $app = Plack::Test->create( App::Route::Capture->to_app );
+
+ {
+ my $res = $app->request( GET '/foo/bar' );
+ ok( $res->is_success, 'Successful request' );
+ }
+};
+
+SKIP: {
+ Test::More::skip "named captures not available until 5.10", 1
+ if !$^V or $^V lt v5.10;
+
+ subtest 'Named captured route parameters' => sub {
+ {
+ package App::Route::NamedCapture; ## no critic
+ use Dancer2;
+ my $re = '^/bar/(?<baz>[^/]+)$';
+ 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();
diff --git a/t/dsl/pass.t b/t/dsl/pass.t
new file mode 100644
index 00000000..9cd67391
--- /dev/null
+++ b/t/dsl/pass.t
@@ -0,0 +1,45 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+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;
diff --git a/t/dsl/path.t b/t/dsl/path.t
new file mode 100644
index 00000000..9a17db31
--- /dev/null
+++ b/t/dsl/path.t
@@ -0,0 +1,125 @@
+use 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' );
+};
+
diff --git a/t/dsl/request.t b/t/dsl/request.t
new file mode 100644
index 00000000..b477f7a4
--- /dev/null
+++ b/t/dsl/request.t
@@ -0,0 +1,42 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ package App::DSL::Request;
+ use Dancer2;
+
+ any [ 'get', 'post' ], '/' => sub {
+ request->method;
+ };
+
+ get 'headers' => sub {
+ request_header 'X-Foo';
+ };
+}
+
+subtest 'Testing an app with request keyword' => sub {
+ my $test = Plack::Test->create( App::DSL::Request->to_app );
+ {
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful GET request' );
+ is( $res->content, 'GET', 'GET / correct content' );
+ }
+ {
+ my $res = $test->request( POST '/' );
+ ok( $res->is_success, 'Successful POST request' );
+ is( $res->content, 'POST', 'POST / correct content' );
+ }
+};
+
+subtest 'Testing app with request_header heyword' => sub {
+ my $test = Plack::Test->create( App::DSL::Request->to_app );
+ my $res = $test->request( GET '/headers', 'X-Foo' => 'Bar' );
+ ok( $res->is_success, 'Successful GET request' );
+ is( $res->content, 'Bar', 'GET /headers correct content' );
+};
+
+done_testing;
+
diff --git a/t/dsl/route_retvals.t b/t/dsl/route_retvals.t
new file mode 100644
index 00000000..5c35d4a1
--- /dev/null
+++ b/t/dsl/route_retvals.t
@@ -0,0 +1,15 @@
+use 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;
diff --git a/t/dsl/send_as.t b/t/dsl/send_as.t
new file mode 100644
index 00000000..0aff1e67
--- /dev/null
+++ b/t/dsl/send_as.t
@@ -0,0 +1,161 @@
+use 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 => '<html></html>'
+ };
+
+ get '/plain' => sub {
+ send_as plain => 'some plain text with <html></html>';
+ };
+
+ get '/json/**' => sub {
+ send_as JSON => splat;
+ };
+
+ get '/json-object' => sub {
+ send_as JSON => { data => DummyObj->new() };
+ };
+
+ get '/json-utf8/**' => sub {
+ send_as JSON => splat, { content_type => 'application/json', charset => 'utf-8' };
+ };
+
+ get '/yaml/**' => sub {
+ my @params = splat;
+ \@params;
+ };
+
+ get '/sendas/:type?' => sub {
+ send_as route_parameters->{'type'} => 'test string';
+ };
+}
+
+my $test = Plack::Test->create( Test::App::SendAs->to_app );
+
+subtest "default serializer" => sub {
+ my $res = $test->request( GET '/yaml/is/useful' );
+ is $res->code, '200';
+ is $res->content_type, 'text/x-yaml';
+
+ my $expected = <<'YAML';
+---
+-
+ - is
+ - useful
+YAML
+
+ is $res->content, $expected;
+
+};
+
+subtest "send_as json" => sub {
+ my $res = $test->request( GET '/json/is/wonderful' );
+ is $res->code, '200';
+ is $res->content_type, 'application/json';
+
+ is $res->content, '["is","wonderful"]';
+};
+
+subtest "send_as json object" => sub {
+ my $res = $test->request( GET '/json-object' );
+ is $res->code, '200';
+ is $res->content_type, 'application/json';
+
+ is $res->content, '{"data":{"foo":"bar"}}';
+};
+
+subtest "send_as json custom content-type" => sub {
+ my $res = $test->request( GET '/json-utf8/is/wonderful' );
+ is $res->code, '200';
+ is $res->content_type, 'application/json';
+ is $res->content_type_charset, 'UTF-8';
+
+ is $res->content, '["is","wonderful"]';
+};
+
+subtest "send_as html" => sub {
+ my $res = $test->request( GET '/html' );
+ is $res->code, '200';
+ is $res->content_type, 'text/html';
+ is $res->content_type_charset, 'UTF-8';
+
+ is $res->content, '<html></html>';
+};
+
+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 <html></html>';
+};
+
+subtest "send_as error cases" => sub {
+ my $logger = Test::App::SendAs::app->logger_engine;
+
+ {
+ my $res = $test->request( GET '/sendas/' );
+ is $res->code, '500', "send_as dies with no defined type";
+
+ my $logs = $logger->trapper->read;
+ like $logs->[0]->{message},
+ qr!Route exception: Can not send_as using an undefined type!,
+ ".. throws route exception";
+ }
+
+ {
+ my $res = $test->request( GET '/sendas/jSoN' );
+ is $res->code, '500',
+ "send_as dies with incorrectly cased serializer name";
+
+ my $logs = $logger->trapper->read;
+ like $logs->[0]->{message},
+ qr!Route exception: Unable to load serializer class for jSoN!,
+ ".. throws route exception";
+ }
+
+ {
+ my $res = $test->request( GET '/sendas/SomeSerializerThatDoesNotExist' );
+ is $res->code, '500',
+ "send_as dies when called with non-existant serializer";
+
+ my $logs = $logger->trapper->read;
+ like $logs->[0]->{message},
+ qr!Route exception: Unable to load serializer class for SomeSerializerThatDoesNotExist!,
+ ".. throws route exception";
+ }
+};
+
+done_testing();
diff --git a/t/dsl/send_file.t b/t/dsl/send_file.t
new file mode 100644
index 00000000..0ea2594b
--- /dev/null
+++ b/t/dsl/send_file.t
@@ -0,0 +1,149 @@
+use strict;
+use warnings;
+use utf8;
+
+use Encode 'encode_utf8';
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use File::Temp;
+use File::Spec;
+use Ref::Util qw<is_coderef>;
+
+{
+ package StaticContent;
+
+ use Dancer2;
+ use Encode 'encode_utf8';
+
+ set views => 't/corpus/static';
+ set public_dir => 't/corpus/static';
+
+ get '/' => sub {
+ send_file 'index.html';
+ };
+
+ prefix '/some' => sub {
+ get '/image' => sub {
+ send_file '1x1.png';
+ return "send_file returns; this content is ignored";
+ };
+ };
+
+ get '/stringref' => sub {
+ my $string = encode_utf8("This is əɯosəʍɐ an test string");
+ send_file( \$string );
+ };
+
+ get '/filehandle' => sub {
+ open my $fh, "<:raw", __FILE__;
+ send_file( $fh, content_type => 'text/plain', charset => 'utf-8' );
+ };
+
+ get '/check_content_type' => sub {
+ my $temp = File::Temp->new();
+ print $temp "hello";
+ close $temp;
+ send_file($temp->filename, content_type => 'image/png',
+ system_path => 1);
+ };
+
+ get '/no_streaming' => sub {
+ my $file = File::Spec->rel2abs(__FILE__);
+ send_file( $file, system_path => 1, streaming => 0 );
+ };
+
+ get '/options_streaming' => sub {
+ my $file = File::Spec->rel2abs(__FILE__);
+ send_file( $file, system_path => 1, streaming => 1 );
+ };
+
+ 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 UTF-8 charset' );
+ my $test_string = encode_utf8('áéíóú');
+ like(
+ $r->content,
+ qr{$test_string},
+ 'Text content contains UTF-8 characters',
+ );
+ };
+
+ subtest "Binary content" => sub {
+ my $r = $cb->( GET '/some/image' );
+
+ is( $r->code, 200, 'send_file sets the status to 200 (binary content)' );
+ unlike( $r->content, qr/send_file returns/,
+ "send_file returns immediately with content");
+ is( $r->header( 'Content-Type' ), 'image/png',
+ 'correct content_type in response' );
+ };
+
+ subtest "string refs" => sub {
+ my $r = $cb->( GET '/stringref' );
+
+ is( $r->code, 200, 'send_file set status to 200 (string ref)');
+ like( $r->content, qr{test string}, 'stringref content' );
+ };
+
+ subtest "filehandles" => sub {
+ my $r = $cb->( GET '/filehandle' );
+
+ is( $r->code, 200, 'send_file set status to 200 (filehandle)');
+ is( $r->content_type, 'text/plain', 'expected content_type');
+ is( $r->content_type_charset, 'UTF-8', 'expected charset');
+ like( $r->content, qr{package StaticContent}, 'filehandle content' );
+ };
+
+ subtest "no streaming" => sub {
+ my $r = $cb->( GET '/no_streaming' );
+ is( $r->code, 200, 'send_file set status to 200 (no streaming)');
+ like( $r->content, qr{package StaticContent}, 'no streaming - content' );
+ };
+
+ subtest "options streaming" => sub {
+ my $r = $cb->( GET '/options_streaming' );
+ is( $r->code, 200, 'send_file set status to 200 (options streaming)');
+ like( $r->content, qr{package StaticContent}, 'options streaming - content' );
+ };
+
+ subtest 'send_file returns correct content type' => sub {
+ my $r = $cb->( GET '/check_content_type' );
+
+ ok($r->is_success, 'send_file returns success');
+ is($r->content_type, 'image/png', 'send_file returns correct content_type');
+ };
+
+ 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');
+ };
+};
+
+done_testing;
diff --git a/t/dsl/splat.t b/t/dsl/splat.t
new file mode 100644
index 00000000..5a76c643
--- /dev/null
+++ b/t/dsl/splat.t
@@ -0,0 +1,30 @@
+use 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 bar baz> ], 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' );
+
diff --git a/t/dsl/to_app.t b/t/dsl/to_app.t
new file mode 100644
index 00000000..47e01e94
--- /dev/null
+++ b/t/dsl/to_app.t
@@ -0,0 +1,31 @@
+use 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' );
+ };
+}
+
+
diff --git a/t/engine.t b/t/engine.t
new file mode 100644
index 00000000..c936338f
--- /dev/null
+++ b/t/engine.t
@@ -0,0 +1,62 @@
+use 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<redefine once>;
+ *Dancer2::Core::Factory::create = sub { $_[1] };
+}
+
+foreach my $engine_type ( qw<logger session template> ) {
+ 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;
diff --git a/t/error.t b/t/error.t
new file mode 100644
index 00000000..6eca1190
--- /dev/null
+++ b/t/error.t
@@ -0,0 +1,264 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'];
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+use List::Util qw<all>;
+
+use Dancer2::Core::App;
+use Dancer2::Core::Response;
+use Dancer2::Core::Request;
+use Dancer2::Core::Error;
+
+use JSON::MaybeXS qw/JSON/; # Error serialization
+
+my $env = {
+ 'psgi.url_scheme' => 'http',
+ REQUEST_METHOD => 'GET',
+ SCRIPT_NAME => '/foo',
+ PATH_INFO => '/bar/baz',
+ REQUEST_URI => '/foo/bar/baz',
+ QUERY_STRING => 'foo=42&bar=12&bar=13&bar=14',
+ SERVER_NAME => 'localhost',
+ SERVER_PORT => 5000,
+ SERVER_PROTOCOL => 'HTTP/1.1',
+ REMOTE_ADDR => '127.0.0.1',
+ HTTP_COOKIE =>
+ 'dancer.session=1234; fbs_102="access_token=xxxxxxxxxx%7Cffffff"',
+ HTTP_X_FORWARDED_FOR => '127.0.0.2',
+ REMOTE_HOST => 'localhost',
+ HTTP_USER_AGENT => 'Mozilla',
+ REMOTE_USER => 'sukria',
+};
+
+my $app = Dancer2::Core::App->new( name => 'main' );
+my $request = $app->build_request($env);
+
+$app->set_request($request);
+
+subtest 'basic defaults of Error object' => sub {
+ my $err = Dancer2::Core::Error->new( app => $app );
+ is $err->status, 500, 'code';
+ is $err->title, 'Error 500 - Internal Server Error', 'title';
+ is $err->message, '', 'message';
+ like $err->content, qr!http://localhost:5000/foo/css!,
+ "error content contains css path relative to uri_base";
+};
+
+subtest "send_error in route" => sub {
+ {
+
+ package App;
+ use Dancer2;
+
+ set serializer => 'JSON';
+
+ get '/error' => sub {
+ send_error "This is a custom error message";
+ return "send_error returns so this content is not processed";
+ };
+ }
+
+ my $app = App->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+ my $r = $cb->( GET '/error' );
+
+ is( $r->code, 500, 'send_error sets the status to 500' );
+ like(
+ $r->content,
+ qr{This is a custom error message},
+ 'Error message looks good',
+ );
+
+ is(
+ $r->content_type,
+ 'application/json',
+ 'Response has appropriate content type after serialization',
+ );
+ };
+};
+
+subtest "send_error with custom stuff" => sub {
+ {
+
+ package App;
+ use Dancer2;
+
+ get '/error/:x' => sub {
+ my $x = param('x');
+ send_error "Error $x", "5$x";
+ };
+ }
+
+ my $app = App->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+ my $r = $cb->( GET '/error/42' );
+
+ is( $r->code, 542, 'send_error sets the status to 542' );
+ like( $r->content, qr{Error 42}, 'Error message looks good' );
+ };
+};
+
+subtest 'Response->error()' => sub {
+ my $resp = Dancer2::Core::Response->new;
+
+ isa_ok $resp->error( message => 'oops', status => 418 ),
+ 'Dancer2::Core::Error';
+
+ is $resp->status => 418, 'response code is 418';
+ like $resp->content => qr/oops/, 'response content overriden by error';
+ like $resp->content => qr/teapot/, 'error code title is present';
+ ok $resp->is_halted, 'response is halted';
+};
+
+subtest 'Throwing an error with a response' => sub {
+ my $resp = Dancer2::Core::Response->new;
+
+ my $err = eval { Dancer2::Core::Error->new(
+ exception => 'our exception',
+ show_errors => 1
+ )->throw($resp) };
+
+ isa_ok($err, 'Dancer2::Core::Response', "Error->throw() accepts a response");
+};
+
+subtest 'Error with show_errors: 0' => sub {
+ my $err = Dancer2::Core::Error->new(
+ exception => 'our exception',
+ show_errors => 0
+ )->throw;
+ unlike $err->content => qr/our exception/;
+};
+
+subtest 'Error with show_errors: 1' => sub {
+ my $err = Dancer2::Core::Error->new(
+ exception => 'our exception',
+ show_errors => 1
+ )->throw;
+ like $err->content => qr/our exception/;
+};
+
+subtest 'App dies with serialized error' => sub {
+ {
+ package AppDies;
+ use Dancer2;
+ set serializer => 'JSON';
+
+ get '/die' => sub {
+ die "oh no\n"; # I should serialize
+ };
+ }
+
+ my $app = AppDies->to_app;
+ isa_ok( $app, 'CODE', 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+ my $r = $cb->( GET '/die' );
+
+ is( $r->code, 500, '/die returns 500' );
+
+ my $out = eval { JSON->new->utf8(0)->decode($r->decoded_content) };
+ ok(!$@, 'JSON decoding serializer error produces no errors');
+ isa_ok($out, 'HASH', 'Error deserializes to a hash');
+ like($out->{exception}, qr/^oh no/, 'Get expected error message');
+ };
+};
+
+subtest 'Error with exception object' => sub {
+ local $@;
+ eval { MyTestException->throw('a test exception object') };
+ my $err = Dancer2::Core::Error->new(
+ exception => $@,
+ show_errors => 1,
+ )->throw;
+
+ like $err->content, qr/a test exception object/, 'Error content contains exception message';
+};
+
+subtest 'Errors without server tokens' => sub {
+ {
+ package AppNoServerTokens;
+ use Dancer2;
+ set serializer => 'JSON';
+ set no_server_tokens => 1;
+
+ get '/ohno' => sub {
+ die "oh no";
+ };
+ }
+
+ my $test = Plack::Test->create( AppNoServerTokens->to_app );
+ my $r = $test->request( GET '/ohno' );
+ is( $r->code, 500, "/ohno returned 500 response");
+ is( $r->header('server'), undef, "No server header when no_server_tokens => 1" );
+};
+
+subtest 'Errors with show_errors and circular references' => sub {
+ {
+ package App::ShowErrorsCircRef;
+ use Dancer2;
+ set show_errors => 1;
+ set something_with_config => {something => config};
+ set password => '===VERY-UNIQUE-STRING===';
+ set innocent_thing => '===VERY-INNOCENT-STRING===';
+ set template => 'simple';
+
+ # 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');
+};
+
+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} }
+}
diff --git a/t/examples/hello_world.t b/t/examples/hello_world.t
new file mode 100644
index 00000000..85929fe9
--- /dev/null
+++ b/t/examples/hello_world.t
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+
+use FindBin ();
+
+use Test::More tests => 3;
+use Plack::Test;
+use HTTP::Request::Common qw(GET);
+
+my $app = do "$FindBin::Bin/../../examples/single/hello_world.psgi";
+is( ref $app, 'CODE', 'Got app' );
+
+my $test = Plack::Test->create($app);
+
+my $res = $test->request( GET '/' );
+ok( $res->is_success, '[GET /] successful' );
+is( $res->content, 'Hello World', 'Content looks ok' );
+
diff --git a/t/examples/simple_calculator.t b/t/examples/simple_calculator.t
new file mode 100644
index 00000000..642acd1a
--- /dev/null
+++ b/t/examples/simple_calculator.t
@@ -0,0 +1,41 @@
+use strict;
+use warnings;
+
+use FindBin ();
+
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common qw(GET POST);
+
+my $app = do "$FindBin::Bin/../../examples/single/simple_calculator.psgi";
+is( ref $app, 'CODE', 'Got app' );
+
+my $test = Plack::Test->create($app);
+
+my $res = $test->request( GET '/' );
+ok( $res->is_success, '[GET /] successful' );
+like( $res->content, qr/powered by Dancer/, 'Content looks ok' );
+
+subtest add => sub {
+ plan tests => 2;
+ my $res = $test->request( GET '/add/19/23' );
+ ok( $res->is_success, '[GET /add/] successful' );
+ is( $res->content, 42, 'Content looks ok' );
+};
+
+subtest multiply => sub {
+ plan tests => 2;
+ my $res = $test->request( GET '/multiply?x=10&y=5' );
+ ok( $res->is_success, '[GET /multiply/] successful' );
+ is( $res->content, 50, 'Content looks ok' );
+};
+
+subtest division => sub {
+ plan tests => 2;
+ my $res = $test->request( POST '/division', { x=>10, y=>5 } );
+ ok( $res->is_success, '[GET /division/] successful' );
+ is( $res->content, 2, 'Content looks ok' );
+};
+
+done_testing();
+
diff --git a/t/factory.t b/t/factory.t
new file mode 100644
index 00000000..eab8828f
--- /dev/null
+++ b/t/factory.t
@@ -0,0 +1,21 @@
+use 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;
diff --git a/t/file_utils.t b/t/file_utils.t
new file mode 100644
index 00000000..bb94efe6
--- /dev/null
+++ b/t/file_utils.t
@@ -0,0 +1,93 @@
+use strict;
+use warnings;
+use utf8;
+
+use Test::More tests => 25;
+use Test::Fatal;
+use File::Spec;
+BEGIN { @File::Spec::ISA = ("File::Spec::Unix") }
+use File::Temp 0.22;
+
+use Dancer2::FileUtils qw/read_file_content path_or_empty path/;
+
+sub write_file {
+ my ( $file, $content ) = @_;
+
+ open my $fh, '>', $file or die "cannot write file $file : $!";
+ binmode $fh, ':encoding(utf-8)';
+ print $fh $content;
+ close $fh;
+}
+
+sub hexe {
+ my $s = shift;
+ $s =~ s/([\x00-\x1F])/sprintf('%#x',ord($1))/eg;
+ return $s;
+}
+
+like(
+ exception { Dancer2::FileUtils::open_file( '<', '/slfkjsdlkfjsdlf' ) },
+ qr{/slfkjsdlkfjsdlf' using mode '<': \w+},
+ 'Failure opening nonexistent file',
+);
+
+my $content = Dancer2::FileUtils::read_file_content();
+is $content, undef;
+
+my $paths = [
+ [ undef => 'undef' ],
+ [ '/foo/./bar/' => '/foo/bar/' ],
+ [ '/foo/../bar' => '/bar' ],
+ [ '/foo/bar/..' => '/foo/' ],
+ [ '/a/b/c/d/A/B/C' => '/a/b/c/d/A/B/C' ],
+ [ '/a/b/c/d/../A/B/C' => '/a/b/c/A/B/C' ],
+ [ '/a/b/c/d/../../A/B/C' => '/a/b/A/B/C' ],
+ [ '/a/b/c/d/../../../A/B/C' => '/a/A/B/C' ],
+ [ '/a/b/c/d/../../../../A/B/C' => '/A/B/C' ],
+];
+
+for my $case ( @$paths ) {
+ is Dancer2::FileUtils::normalize_path( $case->[0] ), $case->[1];
+}
+
+my $p = Dancer2::FileUtils::dirname('/somewhere');
+is $p, '/';
+
+my $tmp = File::Temp->new();
+my $two = "²❷";
+write_file( $tmp, "one$/$two" );
+
+$content = read_file_content($tmp);
+is hexe($content), hexe("one$/$two");
+
+my @content = read_file_content($tmp);
+is hexe( $content[0] ), hexe("one$/");
+is $content[1], "$two";
+
+# returns UNDEF on non-existant path
+my $path = 'bla/blah';
+if ( !-e $path ) {
+ is( path_or_empty($path), '', 'path_or_empty on non-existent path', );
+}
+
+my $tmpdir = File::Temp->newdir;
+is( path_or_empty($tmpdir), $tmpdir, 'path_or_empty on an existing path' );
+
+#slightly tricky paths on different platforms
+is( path( '/', 'b', '/c' ), '/b//c', 'path /,b,/c -> /b//c' );
+is( path( '/', '/b', ), '/b', 'path /, /b -> /b' );
+
+note "escape_filename"; {
+ my $names = [
+ [ undef => 'undef' ],
+ [ 'abcdef' => 'abcdef' ],
+ [ 'ab++ef' => 'ab+2b+2bef' ],
+ [ 'a/../b.txt' => 'a+2f+2e+2e+2fb+2etxt' ],
+ [ "test\0\0" => 'test+00+00' ],
+ [ 'test☠☠☠' => 'test+2620+2620+2620' ],
+ ];
+
+ for my $case ( @$names ) {
+ is Dancer2::FileUtils::escape_filename( $case->[0] ), $case->[1];
+ }
+}
diff --git a/t/forward.t b/t/forward.t
new file mode 100644
index 00000000..d12da8a7
--- /dev/null
+++ b/t/forward.t
@@ -0,0 +1,183 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'];
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+use Dancer2;
+
+set behind_proxy => 1;
+
+get '/' => sub {
+ 'home:' . join( ',', params );
+};
+get '/bounce/' => sub {
+ return forward '/';
+};
+get '/bounce/:withparams/' => sub {
+ return forward '/';
+};
+get '/bounce2/adding_params/' => sub {
+ return forward '/', { withparams => 'foo' };
+};
+post '/simple_post_route/' => sub {
+ 'post:' . join( ',', params );
+};
+get '/go_to_post/' => sub {
+ return forward '/simple_post_route/', { foo => 'bar' },
+ { method => 'post' };
+};
+get '/proxy/' => sub {
+ return uri_for('/');
+};
+get '/forward_with_proxy/' => sub {
+ forward '/proxy/';
+};
+
+# NOT SUPPORTED IN DANCER2
+# In dancer2, vars are alive for only one request flow, a forward initiate a
+# new request flow, then the vars HashRef is destroyed.
+#
+# get '/b' => sub { vars->{test} = 1; forward '/a'; };
+# get '/a' => sub { return "test is " . var('test'); };
+
+my $app = __PACKAGE__->to_app;
+ok( is_coderef($app), 'Got app' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+ is( $cb->( GET '/' )->code, 200, '[GET /] Correct code' );
+ is( $cb->( GET '/' )->content, 'home:', '[GET /] Correct content' );
+
+ is( $cb->( GET '/bounce/' )->code, 200, '[GET /bounce] Correct code' );
+ is(
+ $cb->( GET '/bounce/' )->content,
+ 'home:',
+ '[GET /bounce] Correct content',
+ );
+
+ is(
+ $cb->( GET '/bounce/thesethings/' )->code,
+ 200,
+ '[GET /bounce/thesethings/] Correct code',
+ );
+
+ is(
+ $cb->( GET '/bounce/thesethings/' )->content,
+ 'home:withparams,thesethings',
+ '[GET /bounce/thesethings/] Correct content',
+ );
+
+ is(
+ $cb->( GET '/bounce2/adding_params/' )->code,
+ 200,
+ '[GET /bounce2/adding_params/] Correct code',
+ );
+
+ is(
+ $cb->( GET '/bounce2/adding_params/' )->content,
+ 'home:withparams,foo',
+ '[GET /bounce2/adding_params/] Correct content',
+ );
+
+ is(
+ $cb->( GET '/go_to_post/' )->code,
+ 200,
+ '[GET /go_to_post/] Correct code',
+ );
+
+ is(
+ $cb->( GET '/go_to_post/' )->content,
+ 'post:foo,bar',
+ '[GET /go_to_post/] Correct content',
+ );
+
+ # NOT SUPPORTED
+ # response_status_is [ GET => '/b' ] => 200;
+ # response_content_is [ GET => '/b' ] => 'test is 1';
+
+ {
+ my $res = $cb->( GET '/bounce/' );
+
+ is(
+ $res->headers->content_length,
+ 5,
+ '[GET /bounce/] Correct content length',
+ );
+
+ is(
+ $res->headers->content_type,
+ 'text/html',
+ '[GET /bounce/] Correct content type',
+ );
+
+ is(
+ $res->headers->content_type_charset,
+ 'UTF-8',
+ '[GET /bounce/] Correct content type charset',
+ );
+
+ is(
+ $res->headers->server,
+ "Perl Dancer2 " . Dancer2->VERSION,
+ '[GET /bounce/] Correct Server',
+ );
+
+ }
+
+ # checking post
+ post '/' => sub {'post-home'};
+ post '/bounce/' => sub { forward('/') };
+
+ is( $cb->( POST '/' )->code, 200, '[POST /] Correct code' );
+ is( $cb->( POST '/' )->content, 'post-home', '[POST /] Correct content' );
+
+ is(
+ $cb->( POST '/bounce/' )->code,
+ 200,
+ '[POST /bounce/] Correct code',
+ );
+
+ is(
+ $cb->( POST '/bounce/' )->content,
+ 'post-home',
+ '[POST /bounce/] Correct content',
+ );
+
+ {
+ my $res = $cb->( POST '/bounce/' );
+
+ is(
+ $res->headers->content_length,
+ 9,
+ '[POST /bounce/] Correct content length',
+ );
+
+ is(
+ $res->headers->content_type,
+ 'text/html',
+ '[POST /bounce/] Correct content type',
+ );
+
+ is(
+ $res->headers->content_type_charset,
+ 'UTF-8',
+ '[POST /bounce/] Correct content type charset',
+ );
+
+ is(
+ $res->headers->server,
+ "Perl Dancer2 " . Dancer2->VERSION,
+ '[POST /bounce/] Correct Server',
+ );
+ }
+
+ is(
+ $cb->( GET '/forward_with_proxy/', 'X-Forwarded-Proto' => 'https' )->content,
+ 'https://localhost/',
+ '[GET /forward_with_proxy/] maintained is_behind_proxy',
+ );
+};
+
+done_testing;
diff --git a/t/forward_before_hook.t b/t/forward_before_hook.t
new file mode 100644
index 00000000..1f516fca
--- /dev/null
+++ b/t/forward_before_hook.t
@@ -0,0 +1,58 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'], tests => 4;
+use Dancer2;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+get '/' => sub {
+ return 'Forbidden';
+};
+
+get '/default' => sub {
+ return 'Default';
+};
+
+get '/redirect' => sub {
+ return 'Secret stuff never seen';
+};
+
+hook before => sub {
+ return if request->path eq '/default';
+
+ # Add some content to the response
+ response->content("SillyStringIsSilly");
+
+ # redirect - response should include the above content
+ return redirect '/default'
+ if request->path eq '/redirect';
+
+ # The response object will get replaced by the result of the forward.
+ forward '/default';
+};
+
+my $app = __PACKAGE__->to_app;
+ok( is_coderef($app), 'Got app' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+
+ like(
+ $cb->( GET '/' )->content,
+ qr{Default},
+ 'forward in before hook',
+ );
+
+ my $r = $cb->( GET '/redirect' );
+
+ # redirect in before hook
+ is( $r->code, 302, 'redirect in before hook' );
+ is(
+ $r->content,
+ 'SillyStringIsSilly',
+ '.. and the response content is correct',
+ );
+};
+
+done_testing();
diff --git a/t/forward_hmv_params.t b/t/forward_hmv_params.t
new file mode 100644
index 00000000..42449086
--- /dev/null
+++ b/t/forward_hmv_params.t
@@ -0,0 +1,57 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'];
+use Plack::Test;
+use HTTP::Request::Common;
+
+use utf8;
+use Encode qw();
+
+{
+ 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 = Encode::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 = Encode::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 = Encode::decode( 'UTF-8', $res->content );
+ is $content, 'home:post,1234,foo,bâr', "body parameters available after forward";
+};
+
+done_testing();
diff --git a/t/forward_test_tcp.t b/t/forward_test_tcp.t
new file mode 100644
index 00000000..52a85a89
--- /dev/null
+++ b/t/forward_test_tcp.t
@@ -0,0 +1,82 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+{
+ package App;
+ use Dancer2;
+
+ get '/' => sub {
+ 'home:' . join( ',', params );
+ };
+
+ get '/bounce/' => sub { forward '/' };
+
+ get '/bounce/:withparams/' => sub { forward '/' };
+
+ get '/bounce2/adding_params/' => sub {
+ forward '/', { withparams => 'foo' };
+ };
+
+ get '/go_to_post/' => sub {
+ forward '/simple_post_route/',
+ { foo => 'bar' },
+ { method => 'post' };
+ };
+
+ post '/simple_post_route/' => sub {
+ 'post:' . join( ',', params );
+ };
+
+ post '/' => sub {'post-home'};
+
+ post '/bounce/' => sub { forward '/' };
+}
+
+my $app = Dancer2->psgi_app;
+ok( is_coderef($app), 'Got app' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+
+ my $res = $cb->(GET "/");
+ is $res->code => 200;
+ like $res->content => qr/home:/;
+
+ $res = $cb->(GET "/bounce/");
+ is $res->code => 200;
+ like $res->content => qr/home:/;
+
+ $res = $cb->(GET "/bounce/thesethings/");
+ is $res->code => 200;
+ is $res->content => 'home:withparams,thesethings';
+
+ $res = $cb->(GET "/bounce2/adding_params/");
+ is $res->code => 200;
+ is $res->content => 'home:withparams,foo';
+
+ $res = $cb->(GET "/go_to_post/");
+ is $res->code => 200;
+ is $res->content => 'post:foo,bar';
+
+ $res = $cb->(GET "/bounce/");
+ is $res->header('Content-Length') => 5;
+ is $res->header('Content-Type') => 'text/html; charset=UTF-8';
+ is $res->header('Server') => "Perl Dancer2 " . Dancer2->VERSION;
+
+ $res = $cb->(POST "/");
+ is $res->code => 200;
+ is $res->content => 'post-home';
+
+ $res = $cb->(POST "/bounce/");
+ is $res->code => 200;
+ is $res->content => 'post-home';
+ is $res->header('Content-Length') => 9;
+ is $res->header('Content-Type') => 'text/html; charset=UTF-8';
+ is $res->header('Server') => "Perl Dancer2 " . Dancer2->VERSION;
+};
+
+done_testing();
diff --git a/t/hooks.t b/t/hooks.t
new file mode 100644
index 00000000..498a8255
--- /dev/null
+++ b/t/hooks.t
@@ -0,0 +1,237 @@
+use strict;
+use warnings;
+use Test::More;
+use File::Spec;
+use Plack::Test;
+use HTTP::Request::Common;
+use Capture::Tiny 0.12 'capture_stderr';
+use JSON::MaybeXS;
+
+eval { require Template; 1; }
+ or plan skip_all => 'Template::Toolkit not present';
+
+my $tests_flags = {};
+
+{
+ package App::WithSerializer;
+ use Dancer2;
+ use Ref::Util qw<is_arrayref is_hashref>;
+
+ set serializer => 'JSON';
+
+ my @hooks = qw(
+ before_request
+ after_request
+
+ before_serializer
+ after_serializer
+ );
+
+ for my $hook (@hooks) {
+ hook $hook => sub {
+ $tests_flags->{$hook} ||= 0;
+ $tests_flags->{$hook}++;
+ };
+ }
+
+ get '/' => sub { +{ "ok" => 1 } };
+
+ hook 'before_serializer' => sub {
+ my ($data) = @_; # don't shift, want to alias..
+ if ( is_arrayref($data) ) {
+ push( @{$data}, ( added_in_hook => 1 ) );
+ } elsif ( is_hashref($data) ) {
+ $data->{'added_in_hook'} = 1;
+ } else {
+ $_[0] = +{ 'added_in_hook' => 1 };
+ }
+ };
+
+ get '/forward' => sub { Test::More::note 'About to forward!'; forward '/' };
+
+ get '/redirect' => sub { redirect '/' };
+
+ get '/json' => sub { +[ foo => 42 ] };
+
+ get '/nothing' => sub { return };
+}
+
+{
+ package App::WithFile;
+ use Dancer2;
+ my @hooks = qw<
+ before_file_render
+ after_file_render
+ >;
+
+ for my $hook (@hooks) {
+ hook $hook => sub {
+ $tests_flags->{$hook} ||= 0;
+ $tests_flags->{$hook}++;
+ };
+ }
+
+ get '/send_file' => sub {
+ send_file( File::Spec->rel2abs(__FILE__), system_path => 1 );
+ };
+}
+
+{
+ package App::WithTemplate;
+ use Dancer2;
+ set template => 'tiny';
+
+ my @hooks = qw(
+ before_template_render
+ after_template_render
+ );
+
+ for my $hook (@hooks) {
+ hook $hook => sub {
+ $tests_flags->{$hook} ||= 0;
+ $tests_flags->{$hook}++;
+ };
+ }
+
+ get '/template' => sub {
+ template \"PLOP";
+ };
+}
+
+{
+ package App::WithIntercept;
+ use Dancer2;
+
+ get '/intercepted' => sub {'not intercepted'};
+
+ hook before => sub {
+ response->content('halted by before');
+ halt;
+ };
+}
+
+{
+ package App::WithError;
+ use Dancer2;
+
+ my @hooks = qw(
+ on_route_exception
+ );
+
+ for my $hook (@hooks) {
+ hook $hook => sub {
+ $tests_flags->{$hook} ||= 0;
+ $tests_flags->{$hook}++;
+ };
+ }
+
+ get '/route_exception' => sub {die 'this is a route exception'};
+
+ hook after => sub {
+ # GH#540 - ensure setting default scalar does not
+ # interfere with hook execution (aliasing)
+ $_ = 42;
+ };
+
+ hook on_route_exception => sub {
+ my ($app, $error) = @_;
+ ::is ref($app), 'Dancer2::Core::App';
+ ::like $error, qr/this is a route exception/;
+ };
+
+ hook init_error => sub {
+ my ($error) = @_;
+ ::is ref($error), 'Dancer2::Core::Error';
+ };
+
+ hook before_error => sub {
+ my ($error) = @_;
+ ::is ref($error), 'Dancer2::Core::Error';
+ };
+
+ hook after_error => sub {
+ my ($response) = @_;
+ ::is ref($response), 'Dancer2::Core::Response';
+ ::ok !$response->is_halted;
+ ::like $response->content, qr/Internal Server Error/;
+ };
+}
+
+subtest 'Request hooks' => sub {
+ my $test = Plack::Test->create( App::WithSerializer->to_app );
+ $test->request( GET '/' );
+
+ is( $tests_flags->{before_request}, 1, "before_request was called" );
+ is( $tests_flags->{after_request}, 1, "after_request was called" );
+ is( $tests_flags->{before_serializer}, 1, "before_serializer was called" );
+ is( $tests_flags->{after_serializer}, 1, "after_serializer was called" );
+ is( $tests_flags->{before_file_render}, undef, "before_file_render undef" );
+
+ note 'after hook called once per request';
+ # Get current value of the 'after_request' tests flag.
+ my $current = $tests_flags->{after_request};
+
+ $test->request( GET '/redirect' );
+ is(
+ $tests_flags->{after_request},
+ ++$current,
+ "after_request called after redirect",
+ );
+
+ note 'Serializer hooks';
+
+ $test->request( GET '/forward' );
+ is(
+ $tests_flags->{after_request},
+ ++$current,
+ "after_request called only once after forward",
+ );
+
+ my $res = $test->request( GET '/json' );
+ is( $res->content, '["foo",42,"added_in_hook",1]', 'Response serialized' );
+ is( $tests_flags->{before_serializer}, 4, 'before_serializer was called' );
+ is( $tests_flags->{after_serializer}, 4, 'after_serializer was called' );
+ is( $tests_flags->{before_file_render}, undef, "before_file_render undef" );
+
+ $res = $test->request( GET '/nothing' );
+ is( $res->content, '{"added_in_hook":1}', 'Before hook modified content' );
+ is( $tests_flags->{before_serializer}, 5, 'before_serializer was called with no content' );
+ is( $tests_flags->{after_serializer}, 5, 'after_serializer was called after content changes in hook' );
+};
+
+subtest 'file render hooks' => sub {
+ my $test = Plack::Test->create( App::WithFile->to_app );
+ $test->request( GET '/send_file' );
+ is( $tests_flags->{before_file_render}, 1, "before_file_render was called" );
+ is( $tests_flags->{after_file_render}, 1, "after_file_render was called" );
+};
+
+subtest 'template render hook' => sub {
+ my $test = Plack::Test->create( App::WithTemplate->to_app );
+
+ $test->request( GET '/template' );
+ is(
+ $tests_flags->{before_template_render},
+ 1,
+ "before_template_render was called",
+ );
+
+ is(
+ $tests_flags->{after_template_render},
+ 1,
+ "after_template_render was called",
+ );
+};
+
+subtest 'before can halt' => sub {
+ my $test = Plack::Test->create( App::WithIntercept->to_app );
+ my $resp = $test->request( GET '/intercepted' );
+ is( $resp->content, 'halted by before' );
+};
+
+subtest 'route_exception' => sub {
+ my $test = Plack::Test->create( App::WithError->to_app );
+ capture_stderr { $test->request( GET '/route_exception' ) };
+};
+
+done_testing;
diff --git a/t/http_methods.t b/t/http_methods.t
new file mode 100644
index 00000000..9a890a69
--- /dev/null
+++ b/t/http_methods.t
@@ -0,0 +1,49 @@
+use strict;
+use warnings;
+
+use Test::More tests => 12;
+use Plack::Test;
+use HTTP::Request;
+use Ref::Util qw<is_coderef>;
+
+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<Method Not Allowed>, q<Illegal method should have "Method Not Allowed" in the content> );
+ }
+};
diff --git a/t/http_status.t b/t/http_status.t
new file mode 100644
index 00000000..a9791125
--- /dev/null
+++ b/t/http_status.t
@@ -0,0 +1,41 @@
+use 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";
+};
diff --git a/t/issues/config.yml b/t/issues/config.yml
new file mode 100644
index 00000000..a512f360
--- /dev/null
+++ b/t/issues/config.yml
@@ -0,0 +1 @@
+logger: "Note"
diff --git a/t/issues/gh-1013/gh-1013.t b/t/issues/gh-1013/gh-1013.t
new file mode 100644
index 00000000..29298640
--- /dev/null
+++ b/t/issues/gh-1013/gh-1013.t
@@ -0,0 +1,39 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Fatal;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ ## no critic
+ package TestApp;
+ use Dancer2;
+
+ my $app = app;
+ hook before_template => sub {
+ 1;
+ };
+
+ set template => 'simple';
+
+ get '/' => sub {
+ template t => { hi => 'hello' },
+ };
+};
+
+my $test = Plack::Test->create( TestApp->to_app );
+
+my $res;
+is(
+ exception { $res = $test->request( GET '/' ); },
+ undef,
+ 'Request does not crash',
+);
+
+ok( $res->is_success, 'Request successful' );
+
+chomp( my $content = $res->content );
+is( $content, 'hello', 'Correct content' );
+
+done_testing;
diff --git a/t/issues/gh-1013/views/t.tt b/t/issues/gh-1013/views/t.tt
new file mode 100644
index 00000000..f6f3c2ca
--- /dev/null
+++ b/t/issues/gh-1013/views/t.tt
@@ -0,0 +1 @@
+<% hi %>
diff --git a/t/issues/gh-1046/config.yml b/t/issues/gh-1046/config.yml
new file mode 100644
index 00000000..30b47a4d
--- /dev/null
+++ b/t/issues/gh-1046/config.yml
@@ -0,0 +1 @@
+no_server_tokens: 1
diff --git a/t/issues/gh-1046/gh-1046.t b/t/issues/gh-1046/gh-1046.t
new file mode 100644
index 00000000..b61ebf81
--- /dev/null
+++ b/t/issues/gh-1046/gh-1046.t
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ ## no critic
+ package App;
+ use Dancer2;
+ get '/' => sub {1};
+}
+
+my $test = Plack::Test->create( App->to_app );
+my $res = $test->request( GET '/' );
+
+is(
+ $res->headers->header('Server'),
+ undef,
+ 'Server header not available',
+);
+
+done_testing;
diff --git a/t/issues/gh-1070.t b/t/issues/gh-1070.t
new file mode 100644
index 00000000..b8fa499a
--- /dev/null
+++ b/t/issues/gh-1070.t
@@ -0,0 +1,22 @@
+use strict;
+use warnings;
+use Test::More tests => 2;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ package App;
+ use Dancer2;
+ set show_errors => 1;
+}
+
+my $test = Plack::Test->create( App->to_app );
+
+my $content =
+ $test->request( GET '/nonexistent_path<strong>crazy</strong>' )->content;
+
+like $content, qr{/nonexistent_path&lt;strong&gt;crazy&lt;/strong&gt;},
+ "Escaped message";
+
+unlike $content, qr{/nonexistent_path<strong>crazy</strong>},
+ "No unescaped message";
diff --git a/t/issues/gh-1098.t b/t/issues/gh-1098.t
new file mode 100644
index 00000000..aed9eb75
--- /dev/null
+++ b/t/issues/gh-1098.t
@@ -0,0 +1,93 @@
+use 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_errors => []) },
+ qr/Reference \Q[]\E did not pass type constraint "Bool"/i,
+ "Error->new(show_errors => []) died";
+
+ is exception {
+ Dancer2::Core::Error->new(serializer => undef)
+ },
+ undef,
+ "Error->new(serializer => undef) lived";
+
+ is exception {
+ Dancer2::Core::Error->new(serializer => Dancer2::Serializer::JSON->new)
+ },
+ undef,
+ "Error->new(serializer => Dancer2::Serializer::JSON->new) lived";
+
+ like exception { Dancer2::Core::Error->new(serializer => JSON->new) }, qr/
+ (
+ requires\sthat\sthe\sreference\sdoes\sDancer2::Core::Role::Serializer
+ |
+ did\snot\spass\stype\sconstraint
+ )
+ /x, "Error->new(serializer => JSON->new) died";
+};
+
+subtest 'Core::Response headers isa tests' => sub {
+ plan tests => 5;
+
+ is exception { Dancer2::Core::Response->new },
+ undef, "Response->new lived";
+
+ is exception {
+ Dancer2::Core::Response->new(headers => [Header => 'Content'])
+ },
+ undef,
+ "Response->new( headers => [ Header => 'Content' ] ) lived";
+
+ is exception {
+ Dancer2::Core::Response->new(headers => HTTP::Headers->new)
+ },
+ undef,
+ "Response->new( headers => HTTP::Headers->new ) lived";
+
+ is exception {
+ Dancer2::Core::Response->new(headers => HTTP::Headers::Fast->new)
+ },
+ undef,
+ "Response->new( headers => HTTP::Headers::Fast->new ) lived";
+
+ like exception {
+ Dancer2::Core::Response->new(headers => JSON->new)
+ },
+ qr/coercion.+failed.+not.+array/i,
+ "Response->new( headers => JSON->new ) died";
+};
+
+subtest 'Core::Role::Logger log_level isa tests' => sub {
+ plan tests => 1 + 6 + 1;
+
+ {
+ package TestLogger;
+ use Moo;
+ with 'Dancer2::Core::Role::Logger';
+ sub log { }
+ }
+
+ is exception { TestLogger->new }, undef, "Logger->new lived";
+
+ my @levels = qw/core debug info warn warning error/;
+ foreach my $level (@levels) {
+ is exception { TestLogger->new(log_level => $level) }, undef,
+ "Logger->new(log_level => $level) lives";
+ }
+
+ like exception { TestLogger->new(log_level => 'BadLevel') },
+ qr/Value "BadLevel" did not pass type constraint "Enum/,
+ "Logger->new(log_level => 'BadLevel') died";
+};
diff --git a/t/issues/gh-1216/gh-1216.t b/t/issues/gh-1216/gh-1216.t
new file mode 100644
index 00000000..0b6b04e3
--- /dev/null
+++ b/t/issues/gh-1216/gh-1216.t
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+use lib 't/issues/gh-1216/lib';
+
+use Test::More 'tests' => 2;
+use Test::Fatal qw<exception>;
+use Module::Runtime qw<require_module>;
+
+my $app;
+is(
+ exception {
+ require_module('App');
+ $app = App->to_app;
+ },
+ undef,
+ 'No exception when creating new app',
+);
+
+isa_ok( $app, 'CODE' );
diff --git a/t/issues/gh-1216/lib/App.pm b/t/issues/gh-1216/lib/App.pm
new file mode 100644
index 00000000..fbf2cc8d
--- /dev/null
+++ b/t/issues/gh-1216/lib/App.pm
@@ -0,0 +1,4 @@
+package App;
+use App::Extra;
+use Dancer2;
+1;
diff --git a/t/issues/gh-1216/lib/App/Extra.pm b/t/issues/gh-1216/lib/App/Extra.pm
new file mode 100644
index 00000000..3008b4d3
--- /dev/null
+++ b/t/issues/gh-1216/lib/App/Extra.pm
@@ -0,0 +1,7 @@
+package App::Extra;
+use Dancer2 appname => 'App';
+use Dancer2::Plugin::Null;
+
+get '/' => sub {'OK'};
+
+1;
diff --git a/t/issues/gh-1216/lib/Dancer2/Plugin/Null.pm b/t/issues/gh-1216/lib/Dancer2/Plugin/Null.pm
new file mode 100644
index 00000000..c6214054
--- /dev/null
+++ b/t/issues/gh-1216/lib/Dancer2/Plugin/Null.pm
@@ -0,0 +1,4 @@
+package Dancer2::Plugin::Null;
+use Dancer2::Plugin;
+register_plugin;
+1;
diff --git a/t/issues/gh-1226/gh-1226.t b/t/issues/gh-1226/gh-1226.t
new file mode 100644
index 00000000..56f1ba2f
--- /dev/null
+++ b/t/issues/gh-1226/gh-1226.t
@@ -0,0 +1,26 @@
+use strict;
+use warnings;
+use lib 't/issues/gh-1226/lib';
+
+use Test::More 'tests' => 4 + 9;
+use Test::Fatal qw<exception>;
+use Plack::Test ();
+use Module::Runtime qw<require_module>;
+use HTTP::Request::Common qw<GET>;
+
+my $app;
+is(
+ exception {
+ require_module('App');
+ $app = App->to_app;
+ },
+ undef,
+ 'No exception when creating new app',
+);
+
+isa_ok( $app, 'CODE' );
+
+my $test = Plack::Test->create($app);
+my $response = $test->request( GET '/' );
+is( $response->code, 200, 'Correct response code' );
+is( $response->content, 'OK', 'Correct response content' );
diff --git a/t/issues/gh-1226/lib/App.pm b/t/issues/gh-1226/lib/App.pm
new file mode 100644
index 00000000..a222078b
--- /dev/null
+++ b/t/issues/gh-1226/lib/App.pm
@@ -0,0 +1,4 @@
+package App;
+use Dancer2 appname => 'OtherApp';
+use App::Extra;
+1;
diff --git a/t/issues/gh-1226/lib/App/Extra.pm b/t/issues/gh-1226/lib/App/Extra.pm
new file mode 100644
index 00000000..cdb99c16
--- /dev/null
+++ b/t/issues/gh-1226/lib/App/Extra.pm
@@ -0,0 +1,11 @@
+package App::Extra;
+use Dancer2 'appname' => 'OtherApp';
+use Dancer2::Plugin::Test::AccessDSL;
+
+get '/' => sub {
+ status(500);
+ change_response_status();
+ return 'OK';
+};
+
+1;
diff --git a/t/issues/gh-1226/lib/Dancer2/Plugin/Test/AccessDSL.pm b/t/issues/gh-1226/lib/Dancer2/Plugin/Test/AccessDSL.pm
new file mode 100644
index 00000000..1acffac0
--- /dev/null
+++ b/t/issues/gh-1226/lib/Dancer2/Plugin/Test/AccessDSL.pm
@@ -0,0 +1,54 @@
+package Dancer2::Plugin::Test::AccessDSL;
+use strict;
+use warnings;
+use Dancer2::Plugin;
+
+plugin_keywords('change_response_status');
+
+sub change_response_status {
+ my $self = shift;
+ my $caller = caller(1);
+ ::is( $self->app->name, 'OtherApp', 'Appname is OtherApp' );
+ ::is( $caller, 'App::Extra', 'The caller class is App::Extra' );
+
+ ::ok(
+ ::exception(sub{ $self->app->dsl }),
+ 'Cannot call DSL via app (bc appname is app)',
+ );
+
+ ::ok(
+ ::exception( sub { $self->app->name->dsl } ),
+ 'Cannot call DSL via appname (bc it is not the consumer class)',
+ );
+
+ ::ok(
+ ::exception( sub { OtherApp->status(400) } ),
+ 'Cannot call DSL via appname string (bc it is not the consumer class)',
+ );
+
+ ::is(
+ ::exception( sub { App::Extra::status(400) } ),
+ undef,
+ 'Was able to successfully call the DSL (via consumer class)',
+ );
+
+ ::is(
+ $self->app->response->status(),
+ 400,
+ 'Status was set correctly',
+ );
+
+ ::is(
+ ::exception( sub { $self->dsl->status(200) } ),
+ undef,
+ 'Was able to successfully call the DSL (via plugin->dsl)',
+ );
+
+ ::is(
+ $self->app->response->status(),
+ 200,
+ 'Status was set correctly',
+ );
+}
+
+1;
diff --git a/t/issues/gh-1230/gh-1230.t b/t/issues/gh-1230/gh-1230.t
new file mode 100644
index 00000000..a6cdef38
--- /dev/null
+++ b/t/issues/gh-1230/gh-1230.t
@@ -0,0 +1,26 @@
+use strict;
+use warnings;
+use lib 't/issues/gh-1230/lib';
+
+use Test::More 'tests' => 4 + 5 + 9;
+use Test::Fatal qw<exception>;
+use Plack::Test ();
+use Module::Runtime qw<require_module>;
+use HTTP::Request::Common qw<GET>;
+
+my $app;
+is(
+ exception {
+ require_module('App');
+ $app = App->to_app;
+ },
+ undef,
+ 'No exception when creating new app',
+);
+
+isa_ok( $app, 'CODE' );
+
+my $test = Plack::Test->create($app);
+my $response = $test->request( GET '/' );
+is( $response->code, 200, 'Correct response code' );
+is( $response->content, 'OK', 'Correct response content' );
diff --git a/t/issues/gh-1230/lib/App.pm b/t/issues/gh-1230/lib/App.pm
new file mode 100644
index 00000000..a222078b
--- /dev/null
+++ b/t/issues/gh-1230/lib/App.pm
@@ -0,0 +1,4 @@
+package App;
+use Dancer2 appname => 'OtherApp';
+use App::Extra;
+1;
diff --git a/t/issues/gh-1230/lib/App/Extra.pm b/t/issues/gh-1230/lib/App/Extra.pm
new file mode 100644
index 00000000..f455232e
--- /dev/null
+++ b/t/issues/gh-1230/lib/App/Extra.pm
@@ -0,0 +1,11 @@
+package App::Extra;
+use Dancer2 'appname' => 'OtherApp';
+use Dancer2::Plugin::Test::AccessPluginDSL;
+
+get '/' => sub {
+ status(500);
+ test_change_response_status();
+ return 'OK';
+};
+
+1;
diff --git a/t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessDSL.pm b/t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessDSL.pm
new file mode 100644
index 00000000..0197c23d
--- /dev/null
+++ b/t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessDSL.pm
@@ -0,0 +1,60 @@
+package Dancer2::Plugin::Test::AccessDSL;
+use strict;
+use warnings;
+use Dancer2::Plugin;
+
+plugin_keywords('change_response_status');
+
+sub change_response_status {
+ my $self = shift;
+ my $caller = caller(1);
+
+ ::is( $self->app->name, 'OtherApp', 'Appname is OtherApp' );
+
+ ::is(
+ $caller,
+ 'Dancer2::Plugin::Test::AccessPluginDSL',
+ 'The caller class is the first plugin (AccessPluginDSL)',
+ );
+
+ ::ok(
+ ::exception(sub{ $self->app->dsl }),
+ 'Cannot call DSL via app (bc appname is app)',
+ );
+
+ ::ok(
+ ::exception( sub { $self->app->name->dsl } ),
+ 'Cannot call DSL via appname (bc it is not the consumer class)',
+ );
+
+ ::ok(
+ ::exception( sub { OtherApp->status(400) } ),
+ 'Cannot call DSL via appname string (bc it is not the consumer class)',
+ );
+
+ ::is(
+ ::exception( sub { App::Extra::status(400) } ),
+ undef,
+ 'Was able to successfully call the DSL (via consumer class)',
+ );
+
+ ::is(
+ $self->app->response->status(),
+ 400,
+ 'Status was set correctly',
+ );
+
+ ::is(
+ ::exception( sub { $self->dsl->status(200) } ),
+ undef,
+ 'Was able to successfully call the DSL (via plugin->dsl)',
+ );
+
+ ::is(
+ $self->app->response->status(),
+ 200,
+ 'Status was set correctly',
+ );
+}
+
+1;
diff --git a/t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessPluginDSL.pm b/t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessPluginDSL.pm
new file mode 100644
index 00000000..e962860f
--- /dev/null
+++ b/t/issues/gh-1230/lib/Dancer2/Plugin/Test/AccessPluginDSL.pm
@@ -0,0 +1,33 @@
+package Dancer2::Plugin::Test::AccessPluginDSL;
+use strict;
+use warnings;
+use Dancer2::Plugin;
+use Dancer2::Plugin::Test::AccessDSL;
+
+plugin_keywords('test_change_response_status');
+
+sub test_change_response_status {
+ my $self = shift;
+ my $caller = caller(1);
+ ::is( $self->app->name, 'OtherApp', 'Appname is OtherApp' );
+ ::is( $caller, 'App::Extra', 'The caller class is App::Extra' );
+
+ ::ok(
+ ::exception( sub { App::Extra::change_response_status() } ),
+ 'App does not receive DSL from our inner Plugin',
+ );
+
+ ::is(
+ ::exception( sub { change_response_status() } ),
+ undef,
+ 'Successfully called the plugin DSL (via plugin->dsl)',
+ );
+
+ ::is(
+ $self->app->response->status(),
+ 200,
+ 'Status was set correctly',
+ );
+}
+
+1;
diff --git a/t/issues/gh-1232.t b/t/issues/gh-1232.t
new file mode 100644
index 00000000..05395711
--- /dev/null
+++ b/t/issues/gh-1232.t
@@ -0,0 +1,58 @@
+use 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(); \ No newline at end of file
diff --git a/t/issues/gh-596.t b/t/issues/gh-596.t
new file mode 100644
index 00000000..e19d3dd1
--- /dev/null
+++ b/t/issues/gh-596.t
@@ -0,0 +1,20 @@
+use 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' );
+
diff --git a/t/issues/gh-634.t b/t/issues/gh-634.t
new file mode 100644
index 00000000..01ce44ea
--- /dev/null
+++ b/t/issues/gh-634.t
@@ -0,0 +1,119 @@
+use strict;
+use warnings;
+use Test::More tests=> 3;
+use File::Temp qw/tempdir/;
+use File::Spec;
+
+my $log_dir = tempdir( CLEANUP => 1 );
+
+{
+ package LogDirSpecified;
+ use Dancer2;
+
+ set engines => {
+ logger => {
+ File => {
+ log_dir => $log_dir,
+ file_name => 'test_log.log',
+ }
+ }
+ };
+ set logger => 'file';
+}
+
+{
+ package NonExistLogDirSpecified;
+ use Dancer2;
+
+ set engines => {
+ logger => {
+ File => {
+ log_dir => "$log_dir/notexist",
+ file_name => 'test_log.log',
+ }
+ }
+ };
+ set logger => 'file';
+}
+
+{
+ package LogDirNotSpecified;
+ use Dancer2;
+
+ set logger => 'file';
+}
+
+my $check_cb = sub {
+ my ( $app, $dir, $file ) = @_;
+ my $logger = $app->logger_engine;
+
+ isa_ok( $logger, 'Dancer2::Logger::File' );
+ is(
+ $logger->environment,
+ $app->environment,
+ 'Logger got correct environment',
+ );
+
+ is(
+ $logger->location,
+ $app->config_location,
+ 'Logger got correct location',
+ );
+
+ is(
+ $logger->log_dir,
+ $dir,
+ 'Logger got correct log directory',
+ );
+
+ is(
+ $logger->file_name,
+ $file,
+ 'Logger got correct filename',
+ );
+
+ is(
+ $logger->log_file,
+ File::Spec->catfile( $dir, $file ),
+ 'Logger got correct log file',
+ );
+};
+
+subtest 'test Logger::File with log_dir specified' => sub {
+ plan tests => 6;
+ my $app = [
+ grep { $_->name eq 'LogDirSpecified' } @{ Dancer2->runner->apps }
+ ]->[0];
+
+ $check_cb->( $app, $log_dir, 'test_log.log' );
+};
+
+subtest 'test Logger::File with log_dir NOT specified' => sub {
+ plan tests => 6;
+ my $app = [
+ grep { $_->name eq 'LogDirNotSpecified' } @{ Dancer2->runner->apps }
+ ]->[0];
+
+ $check_cb->(
+ $app,
+ File::Spec->catdir( $app->config_location, 'logs' ),
+ $app->environment . '.log',
+ );
+};
+
+subtest 'test Logger::File with non-existent log_dir specified' => sub {
+ plan tests => 6;
+
+ my $app = [
+ grep { $_->name eq 'NonExistLogDirSpecified'} @{ Dancer2->runner->apps }
+ ]->[0];
+
+ my $logger = $app->logger_engine;
+
+ $check_cb->(
+ $app,
+ "$log_dir/notexist",
+ 'test_log.log',
+ );
+};
+
diff --git a/t/issues/gh-639/fails/.dancer b/t/issues/gh-639/fails/.dancer
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/t/issues/gh-639/fails/.dancer
diff --git a/t/issues/gh-639/fails/config.yml b/t/issues/gh-639/fails/config.yml
new file mode 100644
index 00000000..09b135b9
--- /dev/null
+++ b/t/issues/gh-639/fails/config.yml
@@ -0,0 +1,3 @@
+engines:
+ foo:
+ bar: baz
diff --git a/t/issues/gh-639/fails/issue.t b/t/issues/gh-639/fails/issue.t
new file mode 100644
index 00000000..29fbff5a
--- /dev/null
+++ b/t/issues/gh-639/fails/issue.t
@@ -0,0 +1,13 @@
+use strict;
+use warnings;
+use Test::More tests => 1;
+use Test::Fatal;
+
+require Dancer2;
+
+like(
+ exception { Dancer2->import() },
+ qr{Engine 'foo' is not supported},
+ 'Correct compilation issue',
+);
+
diff --git a/t/issues/gh-639/succeeds/.dancer b/t/issues/gh-639/succeeds/.dancer
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/t/issues/gh-639/succeeds/.dancer
diff --git a/t/issues/gh-639/succeeds/config.yml b/t/issues/gh-639/succeeds/config.yml
new file mode 100644
index 00000000..ea8ef7bf
--- /dev/null
+++ b/t/issues/gh-639/succeeds/config.yml
@@ -0,0 +1,4 @@
+engines:
+ template:
+ foo:
+ bar: baz
diff --git a/t/issues/gh-639/succeeds/issue.t b/t/issues/gh-639/succeeds/issue.t
new file mode 100644
index 00000000..eef18710
--- /dev/null
+++ b/t/issues/gh-639/succeeds/issue.t
@@ -0,0 +1,13 @@
+use strict;
+use warnings;
+use Test::More tests => 1;
+use Test::Fatal;
+
+require Dancer2;
+
+is(
+ exception { Dancer2->import() },
+ undef,
+ 'No compilation issue',
+);
+
diff --git a/t/issues/gh-650/gh-650.t b/t/issues/gh-650/gh-650.t
new file mode 100644
index 00000000..9f5b844d
--- /dev/null
+++ b/t/issues/gh-650/gh-650.t
@@ -0,0 +1,49 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+BEGIN{
+ # undefine ENV vars used as defaults for app environment in these tests
+ delete $ENV{DANCER_ENVIRONMENT};
+ delete $ENV{PLACK_ENV};
+}
+
+{
+ package MyApp;
+
+ use Dancer2;
+
+ set template => 'template_toolkit';
+
+ get '/foo' => sub {
+ template 'environment_setting'
+ };
+ get '/bar' => sub {
+ set environment => 'development';
+ template 'environment_setting'
+ };
+}
+
+my $app = Dancer2->psgi_app;
+ok( is_coderef($app), 'Got app' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+ my $res;
+
+ $res = $cb->(GET '/foo');
+ is $res->code, 200, 'Successful request';
+ like $res->content, qr/development/, 'Correct content';
+
+ $res = $cb->(GET '/bar');
+ is $res->code, 200, 'Successful request';
+ like $res->content, qr/development/, 'Correct content';
+};
+
+done_testing();
diff --git a/t/issues/gh-650/views/environment_setting.tt b/t/issues/gh-650/views/environment_setting.tt
new file mode 100644
index 00000000..77fd25a2
--- /dev/null
+++ b/t/issues/gh-650/views/environment_setting.tt
@@ -0,0 +1 @@
+[% settings.environment %]
diff --git a/t/issues/gh-723.t b/t/issues/gh-723.t
new file mode 100644
index 00000000..7f3d78aa
--- /dev/null
+++ b/t/issues/gh-723.t
@@ -0,0 +1,55 @@
+use 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' );
+};
+
diff --git a/t/issues/gh-730.t b/t/issues/gh-730.t
new file mode 100644
index 00000000..adc519cf
--- /dev/null
+++ b/t/issues/gh-730.t
@@ -0,0 +1,69 @@
+use 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' );
+};
+
diff --git a/t/issues/gh-762.t b/t/issues/gh-762.t
new file mode 100644
index 00000000..a3a4f3cc
--- /dev/null
+++ b/t/issues/gh-762.t
@@ -0,0 +1,41 @@
+use 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();
+
diff --git a/t/issues/gh-762/views/404.tt b/t/issues/gh-762/views/404.tt
new file mode 100644
index 00000000..25c53d71
--- /dev/null
+++ b/t/issues/gh-762/views/404.tt
@@ -0,0 +1,4 @@
+Template selected.
+
+message: [% content %]
+status: [% status %]
diff --git a/t/issues/gh-794.t b/t/issues/gh-794.t
new file mode 100644
index 00000000..3200b5ff
--- /dev/null
+++ b/t/issues/gh-794.t
@@ -0,0 +1,27 @@
+use strict;
+use warnings;
+use Test::More tests => 2;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ package App;
+ use Dancer2;
+ set serializer => 'JSON';
+
+ post '/' => sub { request->data };
+}
+
+my $test = Plack::Test->create( App->to_app );
+
+is(
+ $test->request( POST '/', Content => '{"foo":42}' )->content,
+ '{"foo":42}',
+ 'Correct JSON content in POST',
+);
+
+is(
+ $test->request( POST '/', Content => 'invalid' )->code,
+ 500,
+ 'Failed to decode invalid content',
+);
diff --git a/t/issues/gh-797.t b/t/issues/gh-797.t
new file mode 100644
index 00000000..14a74bb0
--- /dev/null
+++ b/t/issues/gh-797.t
@@ -0,0 +1,56 @@
+use 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();
+
diff --git a/t/issues/gh-799.t b/t/issues/gh-799.t
new file mode 100644
index 00000000..fc186c48
--- /dev/null
+++ b/t/issues/gh-799.t
@@ -0,0 +1,64 @@
+use 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' );
+};
diff --git a/t/issues/gh-811.t b/t/issues/gh-811.t
new file mode 100644
index 00000000..46bc3144
--- /dev/null
+++ b/t/issues/gh-811.t
@@ -0,0 +1,56 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Cookies;
+use HTTP::Request::Common;
+
+eval { require Dancer2::Session::Cookie; 1 }
+ or plan skip_all => 'Dancer2::Session::Cookie probably missing.';
+
+{
+ package App;
+ use Dancer2;
+
+ set engines => {
+ session => {
+ Cookie => { secret_key => 'you cannot buy happiness' }
+ }
+ };
+
+ set session => 'Cookie';
+
+ get '/set' => sub {
+ session foo => 'bar';
+ redirect '/get';
+ };
+
+ get '/get' => sub {
+ my $data = session->data;
+ return to_json $data;
+ };
+}
+
+my $test = Plack::Test->create( App->to_app );
+my $jar = HTTP::Cookies->new;
+my $url = 'http://localhost';
+my $redir;
+
+subtest 'Creating a session' => sub {
+ my $res = $test->request( GET "$url/set" );
+ ok( $res->is_redirect, 'Request causes redirect' );
+ ($redir) = $res->header('Location');
+ is( $redir, "$url/get", 'Redirects to correct url' );
+ $jar->extract_cookies($res);
+ ok( $jar->as_string, 'Received a session cookie' );
+};
+
+subtest 'Retrieving a session' => sub {
+ my $req = GET $redir;
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ ok( $res->is_success, 'Successful request' );
+ is( $res->content, '{"foo":"bar"}', 'Correct response' );
+};
+
+done_testing;
diff --git a/t/issues/gh-931.t b/t/issues/gh-931.t
new file mode 100644
index 00000000..efc85343
--- /dev/null
+++ b/t/issues/gh-931.t
@@ -0,0 +1,75 @@
+# 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();
diff --git a/t/issues/gh-936.t b/t/issues/gh-936.t
new file mode 100644
index 00000000..9b2650cf
--- /dev/null
+++ b/t/issues/gh-936.t
@@ -0,0 +1,33 @@
+use 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();
+
diff --git a/t/issues/gh-936/views/error.tt b/t/issues/gh-936/views/error.tt
new file mode 100644
index 00000000..ff9aa7fb
--- /dev/null
+++ b/t/issues/gh-936/views/error.tt
@@ -0,0 +1 @@
+*CUSTOM ERROR TEMPLATE GOES HERE*
diff --git a/t/issues/gh-944.t b/t/issues/gh-944.t
new file mode 100644
index 00000000..b53470a4
--- /dev/null
+++ b/t/issues/gh-944.t
@@ -0,0 +1,43 @@
+use 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();
+
diff --git a/t/issues/gh-975/config.yml b/t/issues/gh-975/config.yml
new file mode 100644
index 00000000..727e3358
--- /dev/null
+++ b/t/issues/gh-975/config.yml
@@ -0,0 +1 @@
+public_dir: "t/issues/gh-975/test_public_dir"
diff --git a/t/issues/gh-975/gh-975.t b/t/issues/gh-975/gh-975.t
new file mode 100644
index 00000000..af0e452d
--- /dev/null
+++ b/t/issues/gh-975/gh-975.t
@@ -0,0 +1,15 @@
+use strict;
+use warnings;
+use Test::More 'tests' => 2;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ package App;
+ use Dancer2;
+}
+
+my $test = Plack::Test->create( App->to_app );
+my $res = $test->request( GET '/test.txt' );
+ok( $res->is_success, 'Succeeded retrieving file' );
+like( $res->content, qr{^this is test\.txt}, 'Correct file content' );
diff --git a/t/issues/gh-975/test_public_dir/test.txt b/t/issues/gh-975/test_public_dir/test.txt
new file mode 100644
index 00000000..66e61107
--- /dev/null
+++ b/t/issues/gh-975/test_public_dir/test.txt
@@ -0,0 +1 @@
+this is test.txt
diff --git a/t/issues/memleak/die_in_hooks.t b/t/issues/memleak/die_in_hooks.t
new file mode 100644
index 00000000..0648c831
--- /dev/null
+++ b/t/issues/memleak/die_in_hooks.t
@@ -0,0 +1,55 @@
+# reported memory leak without GH issue or RT ticket
+use strict;
+use warnings;
+use Test::More tests => 6;
+use Plack::Test;
+use Capture::Tiny 'capture_stderr';
+use HTTP::Request::Common;
+
+my $called;
+{ package Foo::Destroy; sub DESTROY { $called++ } } ## no critic
+
+{
+ package App; ## no critic
+ use Dancer2;
+ my $env_key = 'psgix.ignoreme.refleak';
+
+ hook before => sub {
+ request->env->{$env_key} = bless {}, 'Foo::Destroy';
+ };
+
+ hook before => sub {
+ ::ok( request->env->{$env_key}, 'Object exists' );
+ ::isa_ok( request->env->{$env_key}, 'Foo::Destroy', 'It is an object' );
+
+ die "whoops";
+ };
+
+ get '/' => sub {'OK'};
+}
+
+my $test = Plack::Test->create( App->to_app );
+my $res;
+my $stderr = capture_stderr { $res = $test->request( GET '/' ) };
+
+ok( ! $res->is_success, 'Request failed' );
+is( $res->code, 500, 'Failure status' );
+is( $called, 1, 'Memory cleaned' );
+
+# double check stderr
+# '[App:21992] error @2015-03-03 16:39:07> Exception caught in 'core.app.before_request' filter: Hook error: whoops at t/issues/memleak/die_in_hooks.t line 25.
+# at lib/Dancer2/Core/App.pm line 848. in (eval 117) l. 1
+# at ...
+# '
+like(
+ $stderr,
+ qr{
+ ^
+ \[App:\d+\] \s error \s [\@\-\d\s:]+> \s
+ \QException caught in 'core.app.before_request' filter:\E \s
+ \QHook error: whoops\E \s
+ [^\n]+ \n \s* # everything until newline + newline
+ at [^\n]+ \n # another such line (there could be more)
+ }x,
+ 'Correct error',
+);
diff --git a/t/issues/vars-in-forward.t b/t/issues/vars-in-forward.t
new file mode 100644
index 00000000..927a78f0
--- /dev/null
+++ b/t/issues/vars-in-forward.t
@@ -0,0 +1,24 @@
+use strict;
+use warnings;
+use Test::More tests => 2;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ package App;
+ use Dancer2;
+ get '/' => sub {
+ var foo => 'bar';
+ forward '/next';
+ };
+
+ get '/next' => sub {
+ vars->{'foo'};
+ };
+}
+
+my $test = Plack::Test->create( App->to_app );
+my $res = $test->request( GET '/' );
+
+ok( $res->is_success, 'Successful response' );
+is( $res->content, 'bar', 'Correct response' );
diff --git a/t/lib/App1.pm b/t/lib/App1.pm
new file mode 100644
index 00000000..f830e291
--- /dev/null
+++ b/t/lib/App1.pm
@@ -0,0 +1,15 @@
+package t::lib::App1;
+use strict;
+use warnings;
+
+use Dancer2;
+use lib '.';
+use t::lib::DancerPlugin;
+
+install_hooks;
+
+get '/app1' => sub {
+ session 'before_plugin';
+};
+
+1;
diff --git a/t/lib/App2.pm b/t/lib/App2.pm
new file mode 100644
index 00000000..31b32796
--- /dev/null
+++ b/t/lib/App2.pm
@@ -0,0 +1,15 @@
+package t::lib::App2;
+use strict;
+use warnings;
+
+use Dancer2;
+use lib '.';
+use t::lib::DancerPlugin;
+
+install_hooks;
+
+get '/app2' => sub {
+ session 'before_plugin';
+};
+
+1;
diff --git a/t/lib/Dancer2/Plugin/Bar.pm b/t/lib/Dancer2/Plugin/Bar.pm
new file mode 100644
index 00000000..57dc475a
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/Bar.pm
@@ -0,0 +1,23 @@
+package Dancer2::Plugin::Bar;
+
+use strict;
+use warnings;
+
+use Dancer2::Plugin;
+
+sub baz { 'bazbazbaz' }
+
+sub BUILD {
+ my $plugin = shift;
+
+ $plugin->app->add_hook( Dancer2::Core::Hook->new(
+ name => 'after',
+ code => sub { my $resp = shift; $resp->content( $resp->content
+ . 'plugin Bar loaded'
+ ) }
+ ));
+}
+
+1;
+
+
diff --git a/t/lib/Dancer2/Plugin/DancerPlugin.pm b/t/lib/Dancer2/Plugin/DancerPlugin.pm
new file mode 100644
index 00000000..e4df3b81
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/DancerPlugin.pm
@@ -0,0 +1,28 @@
+package Dancer2::Plugin::DancerPlugin;
+use strict;
+use warnings;
+
+use Dancer2::Plugin;
+my $counter = 0;
+
+register around_get => sub {
+ my $dsl = shift;
+ $dsl->get(
+ '/foo/plugin' => sub {
+ 'foo plugin';
+ }
+ );
+};
+
+register install_hooks => sub {
+ my $dsl = shift;
+ $dsl->app->add_hook( Dancer2::Core::Hook->new(
+ name => 'before',
+ code => sub {
+ $dsl->session( before_plugin => ++$counter );
+ }
+ ));
+};
+
+register_plugin;
+1;
diff --git a/t/lib/Dancer2/Plugin/DefineKeywords.pm b/t/lib/Dancer2/Plugin/DefineKeywords.pm
new file mode 100644
index 00000000..681d3667
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/DefineKeywords.pm
@@ -0,0 +1,32 @@
+package Dancer2::Plugin::DefineKeywords;
+
+use Dancer2::Plugin;
+
+push @::expected_keywords, 'foo';
+plugin_keywords foo => sub { 'foo' };
+
+push @::expected_keywords, 'bar';
+has bar => (
+ is => 'ro',
+ plugin_keyword => 1,
+ default => sub { 'bar' },
+);
+
+push @::expected_keywords, 'baz', 'bazz';
+has baz => (
+ is => 'ro',
+ plugin_keyword => [ qw/ baz bazz / ],
+);
+
+push @::expected_keywords, 'biz';
+has boz => (
+ is => 'ro',
+ plugin_keyword => 'biz',
+);
+
+push @::expected_keywords, 'quux', 'qiix', 'qox', 'qooox';
+sub quux :PluginKeyword { 'quux' };
+sub qaax :PluginKeyword(qiix) { die "unimplemented" };
+sub qoox :PluginKeyword(qox qooox) { die "unimplemented" };
+
+1;
diff --git a/t/lib/Dancer2/Plugin/EmptyPlugin.pm b/t/lib/Dancer2/Plugin/EmptyPlugin.pm
new file mode 100644
index 00000000..d47f09ba
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/EmptyPlugin.pm
@@ -0,0 +1,10 @@
+package Dancer2::Plugin::EmptyPlugin;
+use Dancer2::Plugin;
+
+# This plugin does nothing.
+# Based on test from @e11it in #510
+
+register_plugin;
+
+1;
+
diff --git a/t/lib/Dancer2/Plugin/Foo.pm b/t/lib/Dancer2/Plugin/Foo.pm
new file mode 100644
index 00000000..b4cadd82
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/Foo.pm
@@ -0,0 +1,52 @@
+package Dancer2::Plugin::Foo;
+
+use strict;
+use warnings;
+
+use Dancer2::Plugin;
+
+plugin_keywords 'truncate_txt';
+
+has something => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ $_[0]->config->{one};
+ },
+);
+
+has size => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ $_[0]->config->{size} || 99;
+ },
+);
+
+has bar => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ scalar $_[0]->app->with_plugin( 'Bar' )
+ },
+ handles => { 'bar_baz' => 'baz' },
+);
+
+sub BUILD {
+ my $plugin = shift;
+
+ $plugin->app->add_hook( Dancer2::Core::Hook->new(
+ name => 'after',
+ code => sub { my $resp = shift; $resp->content( $resp->content
+ . 'added by plugin with something:' . $plugin->something
+ . $plugin->bar_baz
+ ) }
+ ));
+}
+
+sub truncate_txt {
+ my( $plugin, $text ) = @_;
+ return substr $text, 0, $plugin->size;
+}
+
+1;
diff --git a/t/lib/Dancer2/Plugin/FooPlugin.pm b/t/lib/Dancer2/Plugin/FooPlugin.pm
new file mode 100644
index 00000000..7f18cd7b
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/FooPlugin.pm
@@ -0,0 +1,62 @@
+package Dancer2::Plugin::FooPlugin;
+use Dancer2::Plugin;
+
+on_plugin_import {
+ my $dsl = shift;
+ $dsl->get( '/sitemap' => sub { _html_sitemap($dsl) } );
+};
+
+sub _html_sitemap {
+ join( ', ', _retrieve_get_urls(@_) );
+}
+
+register foo_wrap_request => sub {
+ my ($self) = @_;
+ return $self->app->request;
+}, { is_global => 0 };
+
+register foo_route => sub {
+ my ($self) = @_;
+ $self->get( '/foo', sub {'foo'} );
+} => { is_global => 1, prototype => '$@' };
+
+register p_config => sub {
+ my $dsl = shift;
+ my $config = plugin_setting;
+ return $config;
+};
+
+# taken from SiteMap
+sub _retrieve_get_urls {
+ my $dsl = shift;
+ my ( $route, @urls );
+
+ for my $app ( @{ $dsl->runner->apps } ) {
+ my $routes = $app->routes;
+
+ # push the static get routes into an array.
+ get_route:
+ for my $get_route ( @{ $routes->{get} } ) {
+ my $regexp = $get_route->regexp;
+
+ # If the pattern is a true comprehensive regexp or the route
+ # has a :variable element to it, then omit it.
+ next get_route if ( $regexp =~ m/[()[\]|]|:\w/ );
+
+ # If there is a wildcard modifier, then drop it and have the
+ # full route.
+ $regexp =~ s/\?//g;
+
+ # Other than that, its cool to be added.
+ push( @urls, $regexp )
+ if !grep { $regexp =~ m/$_/i }
+ @$Dancer2::Plugin::SiteMap::OMIT_ROUTES;
+ }
+ }
+
+ return sort(@urls);
+}
+
+
+register_plugin;
+1;
diff --git a/t/lib/Dancer2/Plugin/Hookee.pm b/t/lib/Dancer2/Plugin/Hookee.pm
new file mode 100644
index 00000000..d18a4d08
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/Hookee.pm
@@ -0,0 +1,18 @@
+package Dancer2::Plugin::Hookee;
+
+use Dancer2::Plugin;
+
+register_hook 'start_hookee', 'stop_hookee';
+register_hook 'third_hook';
+
+register some_keyword => sub {
+ execute_hook('start_hookee');
+};
+
+register some_other => sub {
+ execute_hook('third_hook');
+};
+
+register_plugin;
+
+1;
diff --git a/t/lib/Dancer2/Plugin/OnPluginImport.pm b/t/lib/Dancer2/Plugin/OnPluginImport.pm
new file mode 100644
index 00000000..120a2538
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/OnPluginImport.pm
@@ -0,0 +1,26 @@
+package Dancer2::Plugin::OnPluginImport;
+use Dancer2::Plugin;
+
+# register keyword
+register some_import => sub { shift->dancer_version };
+
+# register hook
+register_hook qw(imported_plugin);
+
+# add hook. This triggers the $dsl->hooks attribute
+# to be built plugins added after this should still
+# be able to register and add hooks. See #889.
+on_plugin_import {
+ my $dsl = shift;
+
+ $dsl->app->add_hook(
+ Dancer2::Core::Hook->new(
+ name => 'imported_plugin',
+ code => sub { $dsl->dancer_version }
+ )
+ );
+};
+
+register_plugin;
+
+1;
diff --git a/t/lib/Dancer2/Plugin/PluginWithImport.pm b/t/lib/Dancer2/Plugin/PluginWithImport.pm
new file mode 100644
index 00000000..9c2ec627
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/PluginWithImport.pm
@@ -0,0 +1,27 @@
+package Dancer2::Plugin::PluginWithImport;
+# ABSTRACT: a plugin that implement its own import method
+
+=head1 DESCRIPTION
+
+In order to demonstrate that Dancer2::Plugin won't loose the original
+import method of the plugin.
+
+=cut
+
+use strict;
+use warnings;
+
+use Dancer2::Plugin;
+
+my $_stuff = {};
+sub stuff {$_stuff}
+
+$_stuff->{ __PACKAGE__, } = 'imported';
+
+register dancer_plugin_with_import_keyword => sub {
+ 'dancer_plugin_with_import_keyword';
+};
+
+register_plugin;
+
+1;
diff --git a/t/lib/Dancer2/Plugin/Polite.pm b/t/lib/Dancer2/Plugin/Polite.pm
new file mode 100644
index 00000000..fd9f1869
--- /dev/null
+++ b/t/lib/Dancer2/Plugin/Polite.pm
@@ -0,0 +1,44 @@
+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 { 'farewell!' },
+ );
+
+}
+
+sub add_smileys {
+ my( $plugin, $text ) = @_;
+
+ $text =~ s/ (?<= \. ) / $plugin->smiley /xeg;
+
+ return $text;
+}
+
+1;
+
+
+
diff --git a/t/lib/Dancer2/Session/SimpleNoChangeId.pm b/t/lib/Dancer2/Session/SimpleNoChangeId.pm
new file mode 100644
index 00000000..f3eafa59
--- /dev/null
+++ b/t/lib/Dancer2/Session/SimpleNoChangeId.pm
@@ -0,0 +1,41 @@
+package Dancer2::Session::SimpleNoChangeId;
+# ABSTRACT: in-memory session backend for Dancer2
+#
+# This is a version of Dancer2::Session::Simple that does not support
+# _change_id thus using stash data/destroy/reload session
+
+use Moo;
+use Dancer2::Core::Types;
+use Carp;
+
+with 'Dancer2::Core::Role::SessionFactory';
+
+# The singleton that contains all the session objects created
+my $SESSIONS = {};
+
+sub _sessions {
+ my ($self) = @_;
+ return [ keys %{$SESSIONS} ];
+}
+
+sub _retrieve {
+ my ( $class, $id ) = @_;
+ my $s = $SESSIONS->{$id};
+
+ croak "Invalid session ID: $id"
+ if !defined $s;
+
+ return $s;
+}
+
+sub _destroy {
+ my ( $class, $id ) = @_;
+ delete $SESSIONS->{$id};
+}
+
+sub _flush {
+ my ( $class, $id, $data ) = @_;
+ $SESSIONS->{$id} = $data;
+}
+
+1;
diff --git a/t/lib/Foo.pm b/t/lib/Foo.pm
new file mode 100644
index 00000000..7fab1134
--- /dev/null
+++ b/t/lib/Foo.pm
@@ -0,0 +1,8 @@
+package t::lib::Foo;
+use Dancer2;
+
+get '/in_foo' => sub {
+ session('test');
+};
+
+1;
diff --git a/t/lib/MyDancerDSL.pm b/t/lib/MyDancerDSL.pm
new file mode 100644
index 00000000..72eae707
--- /dev/null
+++ b/t/lib/MyDancerDSL.pm
@@ -0,0 +1,36 @@
+package MyDancerDSL;
+
+use Moo;
+use Dancer2::Core::Hook;
+use Dancer2::Core::Error;
+use Dancer2::FileUtils;
+use Carp;
+
+extends 'Dancer2::Core::DSL';
+
+around dsl_keywords => sub {
+ my $orig = shift;
+ my $keywords = $orig->(@_);
+
+ $keywords->{gateau} = { is_global => 0 }; # cookie
+ $keywords->{moteur} = { is_global => 1 }; # engine
+ $keywords->{stop} = { is_global => 0 }; # halt
+ $keywords->{prend} = { is_global => 1, prototype => '@' }; # get
+ $keywords->{envoie} = { is_global => 1, prototype => '$&' }; # post
+ $keywords->{entete} = { is_global => 0 }; #header
+
+ $keywords->{proto} = { is_global => 1, prototype => '&' }; # prototype
+
+ return $keywords;
+};
+
+sub gateau { goto &Dancer2::Core::DSL::cookie }
+sub moteur { goto &Dancer2::Core::DSL::engine }
+sub stop { goto &Dancer2::Core::DSL::halt }
+sub prend { goto &Dancer2::Core::DSL::get }
+sub envoie { goto &Dancer2::Core::DSL::post }
+sub entete { goto &Dancer2::Core::DSL::header }
+
+sub proto { $_[1]->() }
+
+1;
diff --git a/t/lib/PoC/Plugin/Polite.pm b/t/lib/PoC/Plugin/Polite.pm
new file mode 100644
index 00000000..4785f27d
--- /dev/null
+++ b/t/lib/PoC/Plugin/Polite.pm
@@ -0,0 +1,27 @@
+package PoC::Plugin::Polite;
+# ABSTRACT - register Dancer2::Plugin::Polite under a diferent namespace
+
+use Dancer2::Plugin;
+
+has polite => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ $_[0]->app->with_plugin( 'Polite' );
+ },
+ handles => [ qw( smiley add_smileys ) ],
+);
+
+register_hook 'smileys';
+
+plugin_keywords qw(add_smileys hooked_smileys);
+
+sub hooked_smileys {
+ my ($self, @args) = @_;
+ $self->execute_plugin_hook('smileys');
+ $self->add_smileys(@args);
+};
+
+register_plugin;
+1;
+
diff --git a/t/lib/SubApp1.pm b/t/lib/SubApp1.pm
new file mode 100644
index 00000000..6337a738
--- /dev/null
+++ b/t/lib/SubApp1.pm
@@ -0,0 +1,14 @@
+package t::lib::SubApp1;
+use strict;
+use warnings;
+
+use Dancer2;
+use lib 't/lib';
+use Dancer2::Plugin::DancerPlugin;
+install_hooks;
+
+get '/subapp1' => sub {
+ 1;
+};
+
+1;
diff --git a/t/lib/SubApp2.pm b/t/lib/SubApp2.pm
new file mode 100644
index 00000000..16c60a75
--- /dev/null
+++ b/t/lib/SubApp2.pm
@@ -0,0 +1,14 @@
+package t::lib::SubApp2;
+use strict;
+use warnings;
+
+use Dancer2;
+use lib 't/lib';
+use Dancer2::Plugin::DancerPlugin;
+install_hooks;
+
+get '/subapp2' => sub {
+ 2;
+};
+
+1;
diff --git a/t/lib/TestApp.pm b/t/lib/TestApp.pm
new file mode 100644
index 00000000..a60290d2
--- /dev/null
+++ b/t/lib/TestApp.pm
@@ -0,0 +1,125 @@
+package 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{/(?<class> usr | content | post )/(?<action> delete | find )/(?<id> \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;
diff --git a/t/lib/TestPod.pm b/t/lib/TestPod.pm
new file mode 100644
index 00000000..e9b99ec8
--- /dev/null
+++ b/t/lib/TestPod.pm
@@ -0,0 +1,85 @@
+package 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;
diff --git a/t/lib/TestTypeLibrary.pm b/t/lib/TestTypeLibrary.pm
new file mode 100644
index 00000000..60329f0b
--- /dev/null
+++ b/t/lib/TestTypeLibrary.pm
@@ -0,0 +1,8 @@
+package TestTypeLibrary;
+use Type::Library -base, -declare => ('MyDate');
+use Type::Utils -all;
+BEGIN { extends "Dancer2::Core::Types" };
+
+declare MyDate, as StrMatch [qr{\d\d\d\d-\d\d-\d\d}];
+
+1;
diff --git a/t/lib/poc.pm b/t/lib/poc.pm
new file mode 100644
index 00000000..456a223a
--- /dev/null
+++ b/t/lib/poc.pm
@@ -0,0 +1,22 @@
+package 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;
diff --git a/t/lib/poc2.pm b/t/lib/poc2.pm
new file mode 100644
index 00000000..2ab837fd
--- /dev/null
+++ b/t/lib/poc2.pm
@@ -0,0 +1,32 @@
+package 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;
+
diff --git a/t/log_die_before_hook.t b/t/log_die_before_hook.t
new file mode 100644
index 00000000..54f7658e
--- /dev/null
+++ b/t/log_die_before_hook.t
@@ -0,0 +1,38 @@
+use Test::More;
+use strict;
+use warnings;
+use Plack::Test;
+use HTTP::Request::Common;
+use Capture::Tiny 'capture_stderr';
+use Ref::Util qw<is_coderef>;
+
+{
+ package App;
+ use Dancer2;
+
+ set logger => 'console';
+
+ hook 'before' => sub {
+ die 'test die inside a before hook';
+ print STDERR "error message not caught in the before hook\n";
+ };
+
+ get '/' => sub {
+ print STDERR "error message not caught in the route handler\n";
+ };
+}
+
+my $app = App->to_app;
+ok( is_coderef($app), 'Got app' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+ my $message = capture_stderr { $cb->( GET '/' ) };
+
+ like
+ $message,
+ qr/test die inside a before hook/,
+ 'Got error message when a before hook dies';
+};
+
+done_testing;
diff --git a/t/log_levels.t b/t/log_levels.t
new file mode 100644
index 00000000..e8c5389e
--- /dev/null
+++ b/t/log_levels.t
@@ -0,0 +1,128 @@
+#!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 => 'Simple';
+ 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',
+ );
+ }
+};
diff --git a/t/logger.t b/t/logger.t
new file mode 100644
index 00000000..e45a5949
--- /dev/null
+++ b/t/logger.t
@@ -0,0 +1,116 @@
+use Test::More;
+use strict;
+use warnings;
+
+BEGIN {
+
+ # Freeze time at Tue, 15-Jun-2010 00:00:00 GMT
+ *CORE::GLOBAL::time = sub { return 1276560000 }
+}
+
+
+my $_logs = [];
+
+{
+
+ package Dancer2::Logger::Test;
+ use Moo;
+ with 'Dancer2::Core::Role::Logger';
+
+ sub log {
+ my ( $self, $level, $message ) = @_;
+ push @$_logs, $self->format_message( $level, $message );
+ }
+}
+
+my $logger = Dancer2::Logger::Test->new( app_name => 'test' );
+
+is $logger->log_level, 'debug';
+$logger->debug("foo");
+
+# Hard to make caller(6) work when we deal with the logger directly,
+# so do not check for a specific filename.
+like $_logs->[0], qr{debug \@2010-06-1\d \d\d:\d\d:00> foo in };
+
+subtest 'log level and capture' => sub {
+ use Dancer2::Logger::Capture;
+ use Dancer2;
+
+ # NOTE: this will read the config.yml under t/ that defines log level as info
+ set logger => 'capture';
+
+ warning "Danger! Warning!";
+ info "Tango, Foxtrot";
+ debug "I like pie.";
+
+ my $trap = dancer_app->engine('logger')->trapper;
+ my $msg = $trap->read;
+ delete $msg->[0]{'formatted'};
+ delete $msg->[1]{'formatted'};
+ is_deeply $msg,
+ [
+ {
+ level => "warning",
+ message => "Danger! Warning!",
+ },
+ {
+ level => "info",
+ message => "Tango, Foxtrot",
+ },
+ ];
+
+ # each call to read cleans the trap
+ is_deeply $trap->read, [];
+};
+
+subtest 'logger enging hooks' => sub {
+ # before hook can change log level or message.
+ hook 'engine.logger.before' => sub {
+ my $logger = shift; # @_ = ( $level, @message_args )
+ $_[0] = 'panic'; # eg. log all messages at the 'panic' level
+ };
+
+ my $str = "Thou shalt not pass";
+ warning $str;
+ my $trap = dancer_app->engine('logger')->trapper;
+ my $msg = $trap->read;
+ delete $msg->[0]{'formatted'};
+ is_deeply $msg,
+ [
+ {
+ level => "panic",
+ message => $str,
+ },
+ ];
+};
+
+subtest 'logger file' => sub {
+ use Dancer2;
+ use File::Temp qw/tempdir/;
+
+ my $dir = tempdir( CLEANUP => 1 );
+
+ set engines => {
+ logger => {
+ File => {
+ log_dir => $dir,
+ file_name => 'test',
+ }
+ }
+ };
+ # XXX this sucks, we need to set the engine *before* the logger
+ # - Franck, 2013/08/03
+ set logger => 'file';
+
+ warning "Danger! Warning!";
+
+ open my $log_file, '<', File::Spec->catfile($dir, 'test');
+ my $txt = <$log_file>;
+ like $txt, qr/Danger! Warning!/;
+};
+# Explicitly close the logger file handle for those systems that
+# do not allow "open" files to be unlinked (Windows). GH#424.
+my $log_engine = engine('logger');
+close $log_engine->fh;
+
+done_testing;
diff --git a/t/logger_console.t b/t/logger_console.t
new file mode 100644
index 00000000..77665d77
--- /dev/null
+++ b/t/logger_console.t
@@ -0,0 +1,34 @@
+use 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
diff --git a/t/memory_cycles.t b/t/memory_cycles.t
new file mode 100644
index 00000000..f388cf00
--- /dev/null
+++ b/t/memory_cycles.t
@@ -0,0 +1,29 @@
+use 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();
diff --git a/t/mime.t b/t/mime.t
new file mode 100644
index 00000000..c65d1062
--- /dev/null
+++ b/t/mime.t
@@ -0,0 +1,42 @@
+use 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';
diff --git a/t/multi_apps.t b/t/multi_apps.t
new file mode 100644
index 00000000..12967d46
--- /dev/null
+++ b/t/multi_apps.t
@@ -0,0 +1,48 @@
+use 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;
diff --git a/t/multi_apps_forward.t b/t/multi_apps_forward.t
new file mode 100644
index 00000000..4eabd35f
--- /dev/null
+++ b/t/multi_apps_forward.t
@@ -0,0 +1,83 @@
+#!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',
+ );
+ };
+}
+
diff --git a/t/multiapp_template_hooks.t b/t/multiapp_template_hooks.t
new file mode 100644
index 00000000..945673fb
--- /dev/null
+++ b/t/multiapp_template_hooks.t
@@ -0,0 +1,208 @@
+#!perl
+
+use strict;
+use warnings;
+
+use File::Spec;
+use File::Basename 'dirname';
+use Test::More tests => 32;
+use Plack::Test;
+use HTTP::Request::Common;
+
+my $views = File::Spec->rel2abs(
+ File::Spec->catfile( dirname(__FILE__), 'views' )
+);
+
+my %called_hooks = ();
+my $hook_name = 'engine.template.before_render';
+
+{
+ package App1;
+ use Dancer2;
+
+ set views => $views;
+
+ hook before => sub { $called_hooks{'App1'}{'before'}++ };
+
+ hook before_template => sub {
+ my $tokens = shift;
+ ::isa_ok( $tokens, 'HASH', '[App1] Tokens' );
+
+ my $app = app;
+
+ ::isa_ok( $app, 'Dancer2::Core::App', 'Got app object inside App1' );
+
+ # we accept anything that goes to App1, even if not only to App1
+ ::like(
+ $tokens->{'request'}->param('to'),
+ qr/^App1/,
+ 'Request reached to correct App (App1)',
+ );
+
+ ::is(
+ scalar @{ $app->template_engine->hooks->{$hook_name} },
+ 1,
+ 'App1 has a single before_template hook defined',
+ );
+
+ $tokens->{'myname'} = 'App1';
+ $called_hooks{'App1'}{'before_template'}++;
+ };
+
+ get '/' => sub {
+ template beforetemplate => { it => 'App1' }, { layout => undef };
+ };
+}
+
+{
+ package App2;
+ use Dancer2;
+
+ set views => $views;
+
+ hook before => sub { $called_hooks{'App2'}{'before'}++ };
+
+ hook before_template => sub {
+ my $tokens = shift;
+ ::isa_ok( $tokens, 'HASH', '[App2] Tokens' );
+
+ my $app = app;
+ ::isa_ok( $app, 'Dancer2::Core::App', 'Got app object inside App2' );
+
+ ::is(
+ $tokens->{'request'}->param('to'),
+ 'App2',
+ 'Request reached to correct App (App2)',
+ );
+
+ ::is(
+ scalar @{ $app->template_engine->hooks->{$hook_name} },
+ 1,
+ 'App2 has a single before_template hook defined',
+ );
+
+ $tokens->{'myname'} = 'App2';
+ $called_hooks{'App2'}{'before_template'}++;
+ };
+
+ get '/' => sub {
+ template beforetemplate => { it => 'App2' }, { layout => undef };
+ };
+
+ get '/2' => sub {
+ template beforetemplate => { it => 'App2' }, { layout => undef };
+ };
+}
+
+note 'Check App1 only calls first hook, not both'; {
+ # clear
+ %called_hooks = ();
+
+ my $app = App1->to_app;
+ isa_ok( $app, 'CODE', 'Got app for test' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+ my $res = $cb->( GET '/?to=App1' );
+
+ is( $res->code, 200, '[GET /] Successful' );
+
+ is(
+ $res->content,
+ "App is App1, again, it is App1\n",
+ '[GET /] Correct content',
+ );
+
+ is_deeply(
+ \%called_hooks,
+ { App1 => { before => 1, before_template => 1 } },
+ 'Only App1\'s before_template hook was called',
+ );
+ };
+}
+
+note 'Check App2 only calls second hook, not both'; {
+ # clear
+ %called_hooks = ();
+
+ my $app = App2->to_app;
+ isa_ok( $app, 'CODE', 'Got app for test' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+ my $res = $cb->( GET '/?to=App2' );
+
+ is( $res->code, 200, '[GET /] Successful' );
+
+ is(
+ $res->content,
+ "App is App2, again, it is App2\n",
+ '[GET /] Correct content',
+ );
+
+ is_deeply(
+ \%called_hooks,
+ { App2 => { before => 1, before_template => 1 } },
+ 'Only App2\'s before_template hook was called',
+ );
+ };
+}
+
+note 'Check both apps only call the first hook (correct app), not both'; {
+ # clear
+ %called_hooks = ();
+
+ my $app = Dancer2->psgi_app;
+ isa_ok( $app, 'CODE', 'Got app for test' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+ my $res = $cb->( GET '/?to=App1:App2' );
+
+ is( $res->code, 200, '[GET /] Successful' );
+
+ is(
+ $res->content,
+ "App is App1, again, it is App1\n",
+ '[GET /] Correct content',
+ );
+
+ is_deeply(
+ \%called_hooks,
+ { App1 => { before => 1, before_template => 1 } },
+ 'Only App1\'s before_template hook was called (full PSGI app)',
+ );
+ };
+}
+
+note 'Check both apps only call the second hook (correct app), not both'; {
+ # clear
+ %called_hooks = ();
+
+ my $app = Dancer2->psgi_app;
+ isa_ok( $app, 'CODE', 'Got app for test' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+ my $res = $cb->( GET '/2?to=App2' );
+
+ is( $res->code, 200, '[GET /2] Successful' );
+
+ is(
+ $res->content,
+ "App is App2, again, it is App2\n",
+ '[GET /2] Correct content',
+ );
+
+ # INFO: %called_hooks does not contain any counts for App1;
+ # no routes match, so no hooks are called.
+ is_deeply(
+ \%called_hooks,
+ {
+ App2 => { before => 1, before_template => 1 },
+ },
+ 'Only App2\'s before_template hook was called (full PSGI app)',
+ );
+ };
+}
+
diff --git a/t/named_apps.t b/t/named_apps.t
new file mode 100644
index 00000000..83cc5dfa
--- /dev/null
+++ b/t/named_apps.t
@@ -0,0 +1,40 @@
+use 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;
diff --git a/t/no_default_middleware.t b/t/no_default_middleware.t
new file mode 100644
index 00000000..8b91d216
--- /dev/null
+++ b/t/no_default_middleware.t
@@ -0,0 +1,59 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use Plack::Builder;
+use HTTP::Request::Common;
+
+use Plack::Middleware::Head;
+use Plack::Middleware::FixMissingBodyInRedirect;
+
+# No default middleware wrappers
+
+{
+ package MyTestApp;
+ use Dancer2;
+ set no_default_middleware => 1;
+
+ get '/' => sub { return 'some content' };
+
+ get '/redirect' => sub { redirect '/' };
+}
+
+
+subtest 'Head' => sub {
+ my $plain = Plack::Test->create( MyTestApp->to_app );
+ my $res = $plain->request( HEAD '/' );
+ ok( length( $res->content ) > 0, 'HEAD request on unwrapped app has content' );
+
+ my $test = Plack::Test->create(
+ builder {
+ enable 'Head';
+ MyTestApp->to_app;
+ }
+ );
+ my $response = $test->request( HEAD '/' );
+ is( length( $response->content ), 0, 'HEAD request on wrapped app has no content' );
+
+ is( $res->header('Content-Length'),
+ $response->header('Content-Length'),
+ 'HEAD requests have consistent content length header'
+ );
+};
+
+subtest 'FixMissingBodyInRedirect' => sub {
+ my $plain = Plack::Test->create( MyTestApp->to_app );
+ my $res = $plain->request( GET '/redirect' );
+ is( length( $res->content ), 0, 'GET request that redirects on unwrapped app has no content' );
+
+ my $test = Plack::Test->create(
+ builder {
+ enable 'FixMissingBodyInRedirect';
+ MyTestApp->to_app;
+ }
+ );
+ my $response = $test->request( GET '/redirect' );
+ ok( length( $response->content ) > 0, 'GET request that redirects on wrapped app has content' );
+};
+
+done_testing;
diff --git a/t/plugin2/basic-2.t b/t/plugin2/basic-2.t
new file mode 100644
index 00000000..a8fb8b92
--- /dev/null
+++ b/t/plugin2/basic-2.t
@@ -0,0 +1,37 @@
+use 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/;
+}
diff --git a/t/plugin2/basic.t b/t/plugin2/basic.t
new file mode 100644
index 00000000..1cdec462
--- /dev/null
+++ b/t/plugin2/basic.t
@@ -0,0 +1,31 @@
+use 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';
+}
+
diff --git a/t/plugin2/define-keywords.t b/t/plugin2/define-keywords.t
new file mode 100644
index 00000000..b02ef9cb
--- /dev/null
+++ b/t/plugin2/define-keywords.t
@@ -0,0 +1,28 @@
+use strict;
+use warnings;
+
+use lib 't/lib';
+
+use Test::More;
+
+plan skip_all => 'Perl >=5.12 required' if $] < 5.012;
+
+plan tests => 2;
+
+use Dancer2;
+use Dancer2::Plugin::DefineKeywords;
+
+my $plugin = Dancer2::Plugin::DefineKeywords->new( app => undef );
+
+subtest "keywords are registered" => sub {
+ for my $keyword ( @::expected_keywords ) {
+ ok( ( scalar grep { $_ eq $keyword } keys %{ $plugin->keywords } ), $keyword );
+ }
+};
+
+subtest "keywords are recognized" => sub {
+ is foo() => 'foo', 'foo';
+ is bar() => 'bar', 'bar';
+ is quux() => 'quux', 'quux';
+};
+
diff --git a/t/plugin2/find_plugin.t b/t/plugin2/find_plugin.t
new file mode 100644
index 00000000..01555eee
--- /dev/null
+++ b/t/plugin2/find_plugin.t
@@ -0,0 +1,104 @@
+use strict;
+use warnings;
+use Test::More 'tests' => 3;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ package Dancer2::Plugin::Foo;
+ use Dancer2::Plugin;
+
+ BEGIN {
+ has 'foo_message' => (
+ 'is' => 'ro',
+ 'default' => sub {'foo'},
+ );
+
+ plugin_keywords('foo_message');
+ }
+}
+
+{
+ package Dancer2::Plugin::Bar;
+ use Dancer2::Plugin;
+
+ BEGIN {
+ has 'bar_message' => (
+ 'is' => 'ro',
+ 'lazy' => 1,
+ 'default' => sub {
+ my $self = shift;
+ ::isa_ok( $self, 'Dancer2::Plugin::Bar' );
+
+ my $foo = $self->find_plugin('Dancer2::Plugin::Foo')
+ or Carp::croak('Cannot find Dancer2::Plugin::Foo');
+
+ ::isa_ok( $foo, 'Dancer2::Plugin::Foo' );
+ ::can_ok( $foo, 'foo_message' );
+ return $foo->foo_message . ':bar';
+ }
+ );
+
+ plugin_keywords('bar_message');
+ }
+}
+
+{
+ package AppWithFoo;
+ use Dancer2;
+ use Dancer2::Plugin::Foo;
+ get '/' => sub { return foo_message() };
+}
+
+{
+ package AppWithBar;
+ use Dancer2;
+ use Dancer2::Plugin::Bar;
+ set 'logger' => 'Capture';
+ get '/' => sub { return bar_message() };
+}
+
+{
+ package AppWithFooAndBar;
+ use Dancer2;
+ use Dancer2::Plugin::Foo;
+ use Dancer2::Plugin::Bar;
+ get '/' => sub { return bar_message() };
+}
+
+subtest 'Baseline' => sub {
+ my $test = Plack::Test->create( AppWithFoo->to_app );
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful response' );
+ is( $res->content, 'foo', 'Foo plugin works correctly' );
+};
+
+subtest 'When parent plugin not available' => sub {
+ my $test = Plack::Test->create( AppWithBar->to_app );
+ my $res = $test->request( GET '/' );
+
+ ok( !$res->is_success, 'Response failed' );
+
+ my $trap = AppWithBar::app->config()->{'logger'};
+ isa_ok( $trap, 'Dancer2::Logger::Capture' );
+
+ my $trapper = $trap->trapper;
+ my $logs = $trapper->read;
+ isa_ok( $logs, 'ARRAY', 'Found logs' );
+ is( scalar @{$logs}, 1, 'One log message' );
+
+ my $message = $logs->[0];
+ is( $message->{'level'}, 'error' );
+ like(
+ $message->{'message'},
+ qr{\QRoute exception: Cannot find Dancer2::Plugin::Foo\E},
+ 'Correct error',
+ );
+};
+
+subtest 'When both parent and child plugins available' => sub {
+ my $test = Plack::Test->create( AppWithFooAndBar->to_app );
+ my $res = $test->request( GET '/' );
+ ok( $res->is_success, 'Successful response' );
+ is( $res->content, 'foo:bar', 'Bar plugin found Foo and worked' );
+};
diff --git a/t/plugin2/from-config.t b/t/plugin2/from-config.t
new file mode 100644
index 00000000..15584e4f
--- /dev/null
+++ b/t/plugin2/from-config.t
@@ -0,0 +1,92 @@
+use strict;
+use warnings;
+
+use Test::More tests => 8;
+
+{
+package Dancer2::Plugin::FromConfig;
+
+use Dancer2::Plugin;
+
+BEGIN {
+has one => (
+ is => 'ro',
+ from_config => 1,
+);
+
+has three => (
+ is => 'ro',
+ from_config => 'two.three',
+);
+
+has four => (
+ is => 'ro',
+ from_config => 1,
+ default => sub { 'quatre' },
+);
+
+has five => (
+ is => 'ro',
+ from_config => sub { 'cinq' },
+);
+
+has six => (
+ is => 'ro',
+ from_config => sub { 'six' },
+ default => sub { 'AH!' },
+ plugin_keyword => 1,
+);
+
+has [qw(seven eight)] => (
+ is => 'ro',
+ from_config => 1,
+ plugin_keyword => 1,
+);
+
+eval {
+ has [qw(nine ten)] => (
+ is => 'ro',
+ from_config => 1,
+ plugin_keyword => ['nine', 'ten'],
+ );
+};
+our $plugin_keyword_exception = $@;
+
+plugin_keywords qw/ one three four five /;
+
+}
+}
+
+{
+ package MyApp;
+
+ use Dancer2;
+
+ use Dancer2::Plugin::FromConfig;
+
+ set plugins => {
+ FromConfig => {
+ one => 'un',
+ two => {
+ three => 'trois',
+ },
+ seven => 'sept',
+ eight => 'huit',
+ }
+ };
+
+
+ Test::More::is one() => 'un', 'from config';
+ Test::More::is three() => 'trois', 'from config, nested';
+ Test::More::is four() => 'quatre', 'nothing in config, default value';
+ Test::More::is five() => 'cinq', 'from_config a coderef';
+ Test::More::is six() => 'AH!', 'from_config a coderef, no override';
+ Test::More::is seven() => 'sept', 'from_config, defined two fields at once #1';
+ Test::More::is eight() => 'huit', 'from_config, defined two fields at once #2';
+ Test::More::ok $Dancer2::Plugin::FromConfig::plugin_keyword_exception,
+ "defining two fields simultaneously with multiple plugin_keyword values"
+ . " is disallowed";
+}
+
+
+
diff --git a/t/plugin2/hooks.t b/t/plugin2/hooks.t
new file mode 100644
index 00000000..487e0d2d
--- /dev/null
+++ b/t/plugin2/hooks.t
@@ -0,0 +1,82 @@
+use 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';
+};
+
diff --git a/t/plugin2/inside-plugin.t b/t/plugin2/inside-plugin.t
new file mode 100644
index 00000000..0b5d43e1
--- /dev/null
+++ b/t/plugin2/inside-plugin.t
@@ -0,0 +1,73 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+BEGIN {
+ package Dancer2::Plugin::InsidePlugin;
+
+ use Dancer2::Plugin;
+
+ has inside => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $app = shift;
+ return $app->plugin->config->{ja};
+ },
+ );
+}
+
+BEGIN {
+ package Dancer2::Plugin::TestPlugin;
+
+ use Dancer2::Plugin;
+
+ has inside_plugin => (
+ is => 'ro',
+ default => sub {
+ scalar $_[0]->app->with_plugin( 'InsidePlugin' )
+ },
+ handles => [ 'inside' ],
+ );
+}
+
+{
+ package MyApp;
+
+ use Dancer2;
+
+ use Dancer2::Plugin::TestPlugin;
+
+ set plugins => {
+ InsidePlugin => {
+ 'ja' => 'da',
+ },
+ TestPlugin => {
+ 'nein' => 'ne',
+ }
+ };
+}
+
+# check whether both plugins are registered
+my $app = MyApp::app();
+my $plugins = $app->plugins;
+my $plugin_ct = scalar(@$plugins);
+
+ok ($plugin_ct == 2, 'Test number of plugins.')
+ || diag "Found $plugin_ct plugins instead of 2.";
+
+my $test_plugin = $app->with_plugin('TestPlugin');
+my $inside_plugin = $test_plugin->inside_plugin;
+
+isa_ok( $test_plugin, 'Dancer2::Plugin::TestPlugin' );
+isa_ok( $inside_plugin, 'Dancer2::Plugin::InsidePlugin' );
+
+# test configuration values
+is $test_plugin->config->{nein} => 'ne', 'Test config of TestPlugin.'
+ or diag "Found instead of expected 'ne': ", $test_plugin->config->{nein};
+
+is $inside_plugin->config->{ja} => 'da', 'Test config of InsidePlugin.'
+ or diag "Found instead of expected 'da': ", $inside_plugin->config->{ja};
+
+done_testing;
diff --git a/t/plugin2/keywords-hooks-namespace.t b/t/plugin2/keywords-hooks-namespace.t
new file mode 100644
index 00000000..f6e6ebae
--- /dev/null
+++ b/t/plugin2/keywords-hooks-namespace.t
@@ -0,0 +1,63 @@
+BEGIN {
+ package Dancer2::Plugin::Plugin1;
+ use Dancer2::Plugin;
+
+ has one => (
+ is => 'ro',
+ default => sub { 'uno' },
+ plugin_keyword => 1,
+ );
+
+ plugin_hooks 'un';
+}
+
+BEGIN {
+ package Dancer2::Plugin::Plugin2;
+ use Dancer2::Plugin;
+
+ has two => (
+ is => 'ro',
+ default => sub { 'dos' },
+ plugin_keyword => 1,
+ );
+
+ plugin_hooks 'deux';
+
+}
+
+use Test::More;
+
+my %tests = (
+ 'Plugin1' => { keywords => [ 'one' ], hooks => [ 'un' ] },
+ 'Plugin2' => { keywords => [ 'two' ], hooks => [ 'deux' ] },
+);
+
+subtest $_ => sub {
+ my $plugin = join '::', 'Dancer2', 'Plugin', $_;
+
+ is_deeply [ keys %{ $plugin->keywords } ] => $tests{$_}{keywords}, 'keywords';
+ is_deeply [ @{ $plugin->ClassHooks } ] => $tests{$_}{hooks}, 'hooks';
+
+} for sort keys %tests;
+
+
+subtest app_side => sub {
+ package MyApp;
+
+ use Dancer2 '!pass';
+ use Dancer2::Plugin::Plugin1;
+ use Dancer2::Plugin::Plugin2;
+
+ use Test::More;
+
+ Test::More::is one() => 'uno', 'from plugin1';
+ Test::More::is two() => 'dos', 'from plugin2';
+
+ is_deeply { map { ref $_ => [ keys %{ $_->hooks } ] } @{ app()->plugins } },
+ {
+ 'Dancer2::Plugin::Plugin1' => [ 'plugin.plugin1.un' ],
+ 'Dancer2::Plugin::Plugin2' => [ 'plugin.plugin2.deux' ],
+ };
+};
+
+done_testing();
diff --git a/t/plugin2/memory_cycles.t b/t/plugin2/memory_cycles.t
new file mode 100644
index 00000000..ff1d31ba
--- /dev/null
+++ b/t/plugin2/memory_cycles.t
@@ -0,0 +1,36 @@
+use 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 Dancer2::Plugin::Null;
+ use Dancer2::Plugin;
+ plugin_keywords 'nothing';
+ sub nothing {1}
+}
+
+{
+ package MyApp::Cycles;
+ use Dancer2;
+ use Dancer2::Plugin::Null;
+
+ 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();
diff --git a/t/plugin2/no-app-munging.t b/t/plugin2/no-app-munging.t
new file mode 100644
index 00000000..26f34add
--- /dev/null
+++ b/t/plugin2/no-app-munging.t
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+
+use Test::More tests => 1;
+
+{
+ package Dancer2::Plugin::Foo;
+
+ use Dancer2::Plugin;
+}
+
+{
+ package MyRandomModule;
+
+ use Test::More;
+
+ sub app { fail "shouldn't try to run it" };
+
+ use Dancer2::Plugin::Foo ();
+
+}
+
+pass "we survived!";
diff --git a/t/plugin2/no-clobbering.t b/t/plugin2/no-clobbering.t
new file mode 100644
index 00000000..e5d5cd5f
--- /dev/null
+++ b/t/plugin2/no-clobbering.t
@@ -0,0 +1,63 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+BEGIN {
+ package
+ Dancer2::Plugin::Foo;
+
+ use Dancer2::Plugin;
+
+ plugin_keywords 'from_config', 'from_plugin_setting', 'from_indirect';
+
+ sub from_config :PluginKeyword {
+ $_[0]->config->{oops};
+ }
+
+ sub from_plugin_setting :PluginKeyword {
+ plugin_setting()->{oops};
+ }
+
+ sub from_indirect :PluginKeyword {
+ _indirect();
+ }
+
+ sub _indirect {
+ plugin_setting()->{oops};
+ }
+}
+
+{
+ package
+ Alpha;
+
+ use Dancer2 '!pass';
+ use Test::More;
+
+ dancer_app->config->{plugins}{Foo}{oops} = 'alpha';
+
+ use Dancer2::Plugin::Foo;
+
+ is from_config() => 'alpha', 'alpha from config';
+ is from_plugin_setting() => 'alpha', 'alpha from plugin_setting';
+ is from_indirect() => 'alpha', 'alpha from indirect';
+
+}
+{
+ package
+ Beta;
+
+ use Dancer2 '!pass';
+ use Test::More;
+
+ dancer_app->config->{plugins}{Foo}{oops} = 'beta';
+
+ use Dancer2::Plugin::Foo;
+
+ is from_config() => 'beta', 'beta from config';
+ is from_plugin_setting() => 'beta', 'beta from plugin_setting';
+ is from_indirect() => 'beta', 'beta from indirect';
+}
+
+done_testing();
diff --git a/t/plugin2/no-config.t b/t/plugin2/no-config.t
new file mode 100644
index 00000000..1d4abaee
--- /dev/null
+++ b/t/plugin2/no-config.t
@@ -0,0 +1,38 @@
+use 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()";
+
diff --git a/t/plugin2/with-plugins.t b/t/plugin2/with-plugins.t
new file mode 100644
index 00000000..e71604c6
--- /dev/null
+++ b/t/plugin2/with-plugins.t
@@ -0,0 +1,71 @@
+use strict;
+use warnings;
+
+use Test::More tests => 8;
+use Scalar::Util qw/ refaddr /;
+
+{
+ package Dancer2::Plugin::Foo;
+ use Dancer2::Plugin;
+}
+
+{
+ package Dancer2::Plugin::Bar;
+ use Dancer2::Plugin;
+}
+
+{
+ package Dancer2::Plugin::Baz;
+ use Dancer2::Plugin;
+}
+
+{
+ package MyApp;
+ use Dancer2;
+}
+
+my $app = MyApp::app();
+
+my $plugin = $app->with_plugin('Foo');
+
+isa_ok $plugin => 'Dancer2::Plugin';
+
+cmp_ok @{ $app->plugins }, '==', 1, "app has one plugin";
+cmp_ok ref($app->plugins->[0]), 'eq', 'Dancer2::Plugin::Foo' , "app has plugin Foo";
+
+my $same_plugin = $app->with_plugin('Foo');
+
+is refaddr $same_plugin => refaddr $plugin,
+ "plugin is not redefined";
+
+cmp_ok @{ $app->plugins }, '==', 1, "app still has one plugin";
+cmp_ok ref($app->plugins->[0]), 'eq', 'Dancer2::Plugin::Foo' , "app has plugin Foo";
+
+subtest "adding plugin Bar" => sub {
+ my $plugin = $app->with_plugin('Bar');
+
+ isa_ok $plugin => 'Dancer2::Plugin';
+
+ cmp_ok @{ $app->plugins }, '==', 2, "app has two plugins";
+
+ cmp_ok ref($app->plugins->[0]), 'eq', 'Dancer2::Plugin::Foo',
+ "app has plugin Foo";
+ cmp_ok ref($app->plugins->[1]), 'eq', 'Dancer2::Plugin::Bar',
+ "app has plugin Bar";
+};
+
+subtest "adding as an object" => sub {
+ my $plugin = Dancer2::Plugin::Baz->new( app => $app );
+ my $p = $app->with_plugin($plugin);
+
+ is refaddr $p => refaddr $plugin, "it's the same";
+
+ cmp_ok @{ $app->plugins }, '==', 3, "app has three plugins";
+
+ cmp_ok ref($app->plugins->[0]), 'eq', 'Dancer2::Plugin::Foo',
+ "app has plugin Foo";
+ cmp_ok ref($app->plugins->[1]), 'eq', 'Dancer2::Plugin::Bar',
+ "app has plugin Bar";
+ cmp_ok ref($app->plugins->[2]), 'eq', 'Dancer2::Plugin::Baz',
+ "app has plugin Baz";
+};
diff --git a/t/plugin_import.t b/t/plugin_import.t
new file mode 100644
index 00000000..e865afa1
--- /dev/null
+++ b/t/plugin_import.t
@@ -0,0 +1,60 @@
+# plugin_import.t
+
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+{
+ 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;
diff --git a/t/plugin_multiple_apps.t b/t/plugin_multiple_apps.t
new file mode 100644
index 00000000..7e545515
--- /dev/null
+++ b/t/plugin_multiple_apps.t
@@ -0,0 +1,34 @@
+# plugin_multiple_apps.t
+
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+{
+ package App;
+
+ BEGIN {
+ use Dancer2;
+ set session => 'Simple';
+ }
+
+ use lib '.';
+ use t::lib::SubApp1 with => { session => engine('session') };
+
+ use t::lib::SubApp2 with => { session => engine('session') };
+}
+
+my $app = Dancer2->psgi_app;
+ok( is_coderef($app), 'Got app' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+
+ is( $cb->( GET '/subapp1' )->content, 1, '/subapp1' );
+ is( $cb->( GET '/subapp2' )->content, 2, '/subapp2' );
+};
+
+done_testing;
diff --git a/t/plugin_register.t b/t/plugin_register.t
new file mode 100644
index 00000000..44cad9cc
--- /dev/null
+++ b/t/plugin_register.t
@@ -0,0 +1,56 @@
+use 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;
diff --git a/t/plugin_syntax.t b/t/plugin_syntax.t
new file mode 100644
index 00000000..aac03618
--- /dev/null
+++ b/t/plugin_syntax.t
@@ -0,0 +1,184 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'];
+use Plack::Test;
+use HTTP::Request::Common;
+use JSON::MaybeXS;
+use Ref::Util qw<is_coderef>;
+
+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;
diff --git a/t/prepare_app.t b/t/prepare_app.t
new file mode 100644
index 00000000..4b89e08c
--- /dev/null
+++ b/t/prepare_app.t
@@ -0,0 +1,61 @@
+use 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',
+ );
+};
diff --git a/t/psgi_app.t b/t/psgi_app.t
new file mode 100644
index 00000000..0fd69a1c
--- /dev/null
+++ b/t/psgi_app.t
@@ -0,0 +1,92 @@
+#!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 );
+ };
+}
+
diff --git a/t/psgi_app_forward_and_pass.t b/t/psgi_app_forward_and_pass.t
new file mode 100644
index 00000000..84cb3316
--- /dev/null
+++ b/t/psgi_app_forward_and_pass.t
@@ -0,0 +1,49 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use Test::More tests => 4;
+use Plack::Test;
+use HTTP::Request::Common;
+
+{
+ package App1;
+ use Dancer2;
+ get '/' => sub {'App1'};
+}
+
+{
+ package App2;
+ use Dancer2;
+ get '/pass' => sub { pass };
+}
+
+{
+ package App3;
+ use Dancer2;
+ get '/pass' => sub {'App3'};
+ get '/forward' => sub { forward '/' };
+}
+
+# pass from App2 to App3
+# forward from App3 to App1
+my $app = Dancer2->psgi_app;
+isa_ok( $app, 'CODE' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+
+ is( $cb->( GET '/' )->content, 'App1', 'Simple request' );
+
+ is(
+ $cb->( GET '/pass' )->content,
+ 'App3',
+ 'Passing from App to App works',
+ );
+
+ is(
+ $cb->( GET '/forward' )->content,
+ 'App1',
+ 'Forwarding from App to App works',
+ );
+};
diff --git a/t/public/file.txt b/t/public/file.txt
new file mode 100644
index 00000000..71d75fca
--- /dev/null
+++ b/t/public/file.txt
@@ -0,0 +1 @@
+this is a public file
diff --git a/t/redirect.t b/t/redirect.t
new file mode 100644
index 00000000..c6b796da
--- /dev/null
+++ b/t/redirect.t
@@ -0,0 +1,244 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+subtest 'basic redirects' => sub {
+ {
+ package App1;
+ use Dancer2;
+
+ get '/' => sub {'home'};
+ get '/bounce' => sub { redirect '/' };
+ get '/redirect' => sub { response_header 'X-Foo' => 'foo'; redirect '/'; };
+ get '/redirect_querystring' => sub { redirect '/login?failed=1' };
+ }
+
+ my $app = App1->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ {
+ my $res = $cb->( GET '/' );
+
+ is( $res->code, 200, '[GET /] Correct code' );
+ is( $res->content, 'home', '[GET /] Correct content' );
+
+ is(
+ $res->headers->content_type,
+ 'text/html',
+ '[GET /] Correct content-type',
+ );
+
+ is(
+ $cb->( GET '/bounce' )->code,
+ 302,
+ '[GET /bounce] Correct code',
+ );
+ }
+
+ {
+ my $res = $cb->( GET '/redirect' );
+
+ is( $res->code, 302, '[GET /redirect] Correct code' );
+
+ is(
+ $res->headers->header('Location'),
+ 'http://localhost/',
+ 'Correct Location header',
+ );
+
+ is(
+ $res->headers->header('X-Foo'),
+ 'foo',
+ 'Correct X-Foo header',
+ );
+ }
+
+ {
+ my $res = $cb->( GET '/redirect_querystring' );
+
+ is( $res->code, 302, '[GET /redirect_querystring] Correct code' );
+
+ is(
+ $res->headers->header('Location'),
+ 'http://localhost/login?failed=1',
+ 'Correct Location header',
+ );
+ }
+ };
+};
+
+# redirect absolute
+subtest 'absolute and relative redirects' => sub {
+ {
+ package App2;
+ use Dancer2;
+
+ get '/absolute_with_host' =>
+ sub { redirect "http://foo.com/somewhere"; };
+ get '/absolute' => sub { redirect "/absolute"; };
+ get '/relative' => sub { redirect "somewhere/else"; };
+ }
+
+ my $app = App2->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ {
+ my $res = $cb->( GET '/absolute_with_host' );
+
+ is(
+ $res->headers->header('Location'),
+ 'http://foo.com/somewhere',
+ 'Correct Location header',
+ );
+ }
+
+ {
+ my $res = $cb->( GET '/absolute' );
+
+ is(
+ $res->headers->header('Location'),
+ 'http://localhost/absolute',
+ 'Correct Location header',
+ );
+ }
+
+ {
+ my $res = $cb->( GET '/relative' );
+
+ is(
+ $res->headers->header('Location'),
+ 'http://localhost/somewhere/else',
+ 'Correct Location header',
+ );
+ }
+ };
+};
+
+subtest 'redirect behind a proxy' => sub {
+ {
+ package App3;
+ use Dancer2;
+ prefix '/test2';
+ set behind_proxy => 1;
+ get '/bounce' => sub { redirect '/test2' };
+ }
+
+ my $app = App3->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ {
+ is(
+ $cb->(
+ GET '/test2/bounce',
+ 'X-FORWARDED-HOST' => 'nice.host.name',
+ )->headers->header('Location'),
+ 'http://nice.host.name/test2',
+ 'behind a proxy, host() is read from X_FORWARDED_HOST',
+ );
+ }
+
+ {
+ is(
+ $cb->(
+ GET '/test2/bounce',
+ 'X-FORWARDED-HOST' => 'nice.host.name',
+ 'FORWARDED-PROTO' => 'https',
+ )->headers->header('Location'),
+ 'https://nice.host.name/test2',
+ '... and the scheme is read from HTTP_FORWARDED_PROTO',
+ );
+ }
+
+ {
+ is(
+ $cb->(
+ GET '/test2/bounce',
+ 'X-FORWARDED-HOST' => 'nice.host.name',
+ 'X-FORWARDED-PROTOCOL' => 'ftp', # stupid, but why not?
+ )->headers->header('Location'),
+ 'ftp://nice.host.name/test2',
+ '... or from X_FORWARDED_PROTOCOL',
+ );
+ }
+
+ {
+ is(
+ $cb->(
+ GET '/test2/bounce',
+ 'X-FORWARDED-HOST' => 'nice.host.name',
+ 'X-FORWARDED-PROTO' => 'https',
+ )->headers->header('Location'),
+ 'https://nice.host.name/test2',
+ '... or from X_FORWARDED_PROTO',
+ );
+ }
+ };
+};
+
+subtest 'redirect behind multiple proxies' => sub {
+ {
+
+ package App4;
+ use Dancer2;
+ prefix '/test2';
+ set behind_proxy => 1;
+ get '/bounce' => sub { redirect '/test2' };
+ }
+
+ my $app = App4->to_app;
+ ok( is_coderef($app), 'Got app' );
+
+ test_psgi $app, sub {
+ my $cb = shift;
+
+ {
+ is(
+ $cb->(
+ GET '/test2/bounce',
+ 'X-FORWARDED-HOST' => "proxy1.example, proxy2.example",
+ )->headers->header('Location'),
+ 'http://proxy1.example/test2',
+ "behind multiple proxies, host() is read from X_FORWARDED_HOST",
+ );
+ }
+
+ {
+ is(
+ $cb->(
+ GET '/test2/bounce',
+ 'X-FORWARDED-HOST' => "proxy1.example, proxy2.example",
+ 'FORWARDED-PROTO' => 'https',
+ )->headers->header('Location'),
+ 'https://proxy1.example/test2',
+ '... and the scheme is read from HTTP_FORWARDED_PROTO',
+ );
+ }
+
+ {
+ is(
+ $cb->(
+ GET '/test2/bounce',
+ 'X-FORWARDED-HOST' => "proxy1.example, proxy2.example",
+ 'X-FORWARDED-PROTOCOL' => 'ftp', # stupid, but why not?
+ )->headers->header('Location'),
+ 'ftp://proxy1.example/test2',
+ '... or from X_FORWARDED_PROTOCOL',
+ );
+ }
+ };
+};
+
+done_testing;
diff --git a/t/release-distmeta.t b/t/release-distmeta.t
new file mode 100644
index 00000000..ff0d696d
--- /dev/null
+++ b/t/release-distmeta.t
@@ -0,0 +1,14 @@
+#!perl
+
+BEGIN {
+ unless ($ENV{RELEASE_TESTING}) {
+ print qq{1..0 # SKIP these tests are for release candidate testing\n};
+ exit
+ }
+}
+
+# This file was automatically generated by Dist::Zilla::Plugin::MetaTests.
+
+use Test::CPAN::Meta;
+
+meta_yaml_ok();
diff --git a/t/request.t b/t/request.t
new file mode 100644
index 00000000..4e087f98
--- /dev/null
+++ b/t/request.t
@@ -0,0 +1,273 @@
+use 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;
diff --git a/t/request_make_forward_to.t b/t/request_make_forward_to.t
new file mode 100644
index 00000000..ac06b379
--- /dev/null
+++ b/t/request_make_forward_to.t
@@ -0,0 +1,38 @@
+use strict;
+use warnings;
+use Test::More tests=>1;
+use Plack::Test;
+use HTTP::Request::Common;
+use JSON::MaybeXS;
+
+{
+
+ package ContentLengthTestApp;
+ use Dancer2;
+ set serializer => 'JSON';
+
+ post '/foo' => sub {
+ forward('/not_authorized');
+ };
+
+ any '/not_authorized' => sub {
+ status 401;
+ { access => 'denied' };
+ };
+}
+
+{
+ my $url = 'http://localhost';
+ my $test = Plack::Test->create( ContentLengthTestApp->to_app );
+
+ my $response = $test->request(
+ POST(
+ '/foo',
+ Content =>
+ encode_json( { target => [ 'foo', 'bar' ] } ),
+ )
+ );
+
+ is( $response->code, 401, 'Access denied to unauthorized merge' );
+
+}
diff --git a/t/request_upload.t b/t/request_upload.t
new file mode 100644
index 00000000..9c49ba43
--- /dev/null
+++ b/t/request_upload.t
@@ -0,0 +1,196 @@
+use strict;
+use warnings FATAL => 'all';
+use Test::More;
+use Test::Fatal;
+
+use Dancer2::Core::Request;
+
+use Carp;
+use File::Temp 0.22;
+use File::Basename qw/dirname basename/;
+use File::Spec;
+use Encode qw(encode_utf8);
+
+diag "If you want extra speed, install URL::Encode::XS"
+ if !$Dancer2::Core::Request::XS_URL_DECODE;
+diag "If you want extra speed, install CGI::Deurl::XS"
+ if !$Dancer2::Core::Request::XS_PARSE_QUERY_STRING;
+
+sub test_path {
+ my ( $file, $dir ) = @_;
+ is dirname($file), $dir, "dir of $file is $dir";
+}
+
+sub run_test {
+ my $filename = "some_\x{1A9}_file.txt";
+
+ my $content = qq{------BOUNDARY
+Content-Disposition: form-data; name="test_upload_file"; filename="$filename"
+Content-Type: text/plain
+
+SHOGUN
+------BOUNDARY
+Content-Disposition: form-data; name="test_upload_file"; filename="yappo2.txt"
+Content-Type: text/plain
+
+SHOGUN2
+------BOUNDARY
+Content-Disposition: form-data; name="test_upload_file3"; filename="yappo3.txt"
+Content-Type: text/plain
+
+SHOGUN3
+------BOUNDARY
+Content-Disposition: form-data; name="test_upload_file4"; filename="yappo4.txt"
+Content-Type: text/plain
+
+SHOGUN4
+------BOUNDARY
+Content-Disposition: form-data; name="test_upload_file4"; filename="yappo5.txt"
+Content-Type: text/plain
+
+SHOGUN4
+------BOUNDARY
+Content-Disposition: form-data; name="test_upload_file6"; filename="yappo6.txt"
+Content-Type: text/plain
+
+SHOGUN6
+------BOUNDARY--
+};
+ $content =~ s/\r\n/\n/g;
+ $content =~ s/\n/\r\n/g;
+ $content = encode_utf8($content);
+
+
+ do {
+ open my $in, '<', \$content;
+ my $req = Dancer2::Core::Request->new(
+ env => {
+ 'psgi.input' => $in,
+ CONTENT_LENGTH => length($content),
+ CONTENT_TYPE => 'multipart/form-data; boundary=----BOUNDARY',
+ REQUEST_METHOD => 'POST',
+ SCRIPT_NAME => '/',
+ SERVER_PORT => 80,
+ }
+ );
+
+ my @undef = $req->upload('undef');
+ is @undef, 0, 'non-existent upload as array is empty';
+ my $undef = $req->upload('undef');
+ is $undef, undef, '... and non-existent upload as scalar is undef';
+
+ my @uploads = $req->upload('test_upload_file');
+ like $uploads[0]->content, qr|^SHOGUN|,
+ "content for first upload is ok, via 'upload'";
+ like $uploads[1]->content, qr|^SHOGUN|,
+ "... content for second as well";
+ is $req->uploads->{'test_upload_file4'}[0]->content, 'SHOGUN4',
+ "... content for other also good";
+
+ note "headers 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 = File::Spec->catfile( $dest_dir, $upload->basename );
+ $upload->copy_to($dest_file);
+ ok( ( -f $dest_file ), "file '$dest_file' has been copied" );
+
+ my $dest_file_link = File::Spec->catfile( $dest_dir, "hardlink" );
+ $upload->link_to($dest_file_link);
+ ok( ( -f $dest_file_link ),
+ "hardlink '$dest_file_link' has been created"
+ );
+
+ # make sure cleanup is performed when the HTTP::Body object is purged
+ my $file = $upload->tempname;
+ ok( ( -f $file ), 'temp file exists while request object lives' );
+ 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::open_file"}{CODE};
+ no warnings 'redefine';
+ *{"Dancer2::Core::Request::Upload::open_file"} = sub {
+ croak "Can't open mocked-tempfile using mode '<'";
+ };
+ }
+ $upload->{_fh} = undef;
+ like(
+ exception { $upload->file_handle },
+ qr{Can't open.* using mode '<'},
+ );
+
+ # unmock open_file
+ {
+ no strict 'refs';
+ no warnings 'redefine';
+ *{"Dancer2::Core::Request::Upload::open_file"} =
+ $upload_file_coderef;
+ }
+ }
+
+ unlink($file) if ( $^O eq 'MSWin32' );
+ };
+}
+
+note "Run test with XS_URL_DECODE" if $Dancer2::Core::Request::XS_URL_DECODE;
+note "Run test with XS_PARSE_QUERY_STRING"
+ if $Dancer2::Core::Request::XS_PARSE_QUERY_STRING;
+run_test();
+if ($Dancer2::Core::Request::XS_PARSE_QUERY_STRING) {
+ note "Run test without XS_PARSE_QUERY_STRING";
+ $Dancer2::Core::Request::XS_PARSE_QUERY_STRING = 0;
+ $Dancer2::Core::Request::_count = 0;
+ run_test();
+}
+if ($Dancer2::Core::Request::XS_URL_DECODE) {
+ note "Run test without XS_URL_DECODE";
+ $Dancer2::Core::Request::XS_URL_DECODE = 0;
+ $Dancer2::Core::Request::_count = 0;
+ run_test();
+}
+
+done_testing;
diff --git a/t/response.t b/t/response.t
new file mode 100644
index 00000000..6e5dafcd
--- /dev/null
+++ b/t/response.t
@@ -0,0 +1,65 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'];
+use Dancer2;
+use Dancer2::Core::Response;
+
+my $r = Dancer2::Core::Response->new( content => "hello" );
+is $r->status, 200;
+is $r->content, 'hello';
+
+note "content_type";
+$r = Dancer2::Core::Response->new(
+ headers => [ 'Content-Type' => 'text/html' ],
+ content => 'foo',
+);
+
+is_deeply $r->to_psgi,
+ [ 200,
+ [ Server => "Perl Dancer2 " . Dancer2->VERSION,
+ 'Content-Length' => 3,
+ 'Content-Type' => 'text/html',
+ ],
+ ['foo']
+ ];
+
+isa_ok $r->headers, 'HTTP::Headers';
+is $r->content_type, 'text/html';
+
+$r->content_type('text/plain');
+is $r->content_type, 'text/plain';
+
+ok( !$r->is_forwarded );
+$r->forward('http://perldancer.org');
+ok( $r->is_forwarded );
+
+is $r->header('X-Foo'), undef;
+
+$r->header( 'X-Foo' => 42 );
+is $r->header('X-Foo'), 42;
+
+$r->header( 'X-Foo' => 432 );
+is $r->header('X-Foo'), 432;
+
+$r->push_header( 'X-Foo' => 777 );
+is $r->header('X-Foo'), '432, 777';
+
+$r->header( 'X-Bar' => 234 );
+is $r->header('X-Bar'), '234';
+
+is scalar( @{ $r->headers_to_array } ), 12;
+
+# stringify HTTP status
+$r = Dancer2::Core::Response->new( content => "foo", status => "Not Found" );
+is $r->status, 404;
+
+$r =
+ Dancer2::Core::Response->new( content => "foo", status => "not_modified" );
+is $r->status, 304;
+
+# test setting content as "0"
+$r = Dancer2::Core::Response->new( content => "foo" );
+$r->content("0");
+is $r->content, "0";
+
+done_testing;
diff --git a/t/roles/hook.t b/t/roles/hook.t
new file mode 100644
index 00000000..eabdabef
--- /dev/null
+++ b/t/roles/hook.t
@@ -0,0 +1,67 @@
+use 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';
diff --git a/t/route-pod-coverage/route-pod-coverage.t b/t/route-pod-coverage/route-pod-coverage.t
new file mode 100644
index 00000000..a2e91a62
--- /dev/null
+++ b/t/route-pod-coverage/route-pod-coverage.t
@@ -0,0 +1,27 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib '.';
+use t::lib::TestPod;
+use Dancer2::Test apps => ['t::lib::TestPod'];
+
+is_pod_covered 'is pod covered';
+
+my $pod_structure = {
+ 't::lib::TestPod' => {
+ 'has_pod' => 1,
+ 'routes' => [
+ "post /in_testpod/*",
+ "post /me:id",
+ "get /in_testpod",
+ "get /hello",
+ "get /me:id"
+ ]
+ }
+};
+
+is_deeply( route_pod_coverage, $pod_structure, 'my pod looks like expected' );
+
+done_testing;
diff --git a/t/scope_problems/config.yml b/t/scope_problems/config.yml
new file mode 100644
index 00000000..a512f360
--- /dev/null
+++ b/t/scope_problems/config.yml
@@ -0,0 +1 @@
+logger: "Note"
diff --git a/t/scope_problems/dispatcher_internal_request.t b/t/scope_problems/dispatcher_internal_request.t
new file mode 100644
index 00000000..51e37a56
--- /dev/null
+++ b/t/scope_problems/dispatcher_internal_request.t
@@ -0,0 +1,32 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common qw/GET/;
+use Dancer2;
+
+{
+ package Test::App;
+ use Dancer2;
+
+ use Data::Dumper;
+ set behind_proxy => 1;
+ set views => 't/views';
+
+ # The 'die' was causing the Runners' internal_request
+ # object to not get cleaned up when returning from dispatch.
+ hook before => sub { die "Nope, Nope, Nope" };
+
+ get '/' => sub {
+ send_error "Yes yes YES!";
+ };
+}
+
+my $test = Plack::Test->create(Dancer2->psgi_app);
+
+my $res = $test->request(GET '/');
+is( Dancer2->runner->{'internal_request'}, undef,
+ "Runner internal request cleaned up" );
+
+done_testing;
+
diff --git a/t/scope_problems/keywords_before_template_hook.t b/t/scope_problems/keywords_before_template_hook.t
new file mode 100644
index 00000000..166b36d7
--- /dev/null
+++ b/t/scope_problems/keywords_before_template_hook.t
@@ -0,0 +1,43 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common qw/GET/;
+use File::Basename 'dirname';
+use File::Spec;
+
+my $views;
+BEGIN {
+ $views = File::Spec->rel2abs( File::Spec->catfile( dirname(__FILE__), 'views' ) );
+}
+
+eval { require Template; Template->import(); 1 }
+ or plan skip_all => 'Template::Toolkit probably missing.';
+
+{
+ package Test::App;
+ use Dancer2;
+
+ set views => $views;
+ set logger => 'Note';
+ set template => 'template_toolkit';
+
+ hook before_template_render => sub {
+ my $tokens = shift;
+ var some_var => 21; # var can only be used in a route handler..
+ };
+
+ get '/' => sub {
+ die "Yes yes YES!";
+ };
+}
+
+my $test = Plack::Test->create(Test::App->to_app);
+
+my $res = $test->request(GET '/');
+is($res->code, 500, "Got 500 response");
+like( $res->content, qr/This is a dummy error template/,
+ "with the template content" );
+
+done_testing;
+
diff --git a/t/scope_problems/session_is_cleared.t b/t/scope_problems/session_is_cleared.t
new file mode 100644
index 00000000..2bfd687a
--- /dev/null
+++ b/t/scope_problems/session_is_cleared.t
@@ -0,0 +1,65 @@
+#!/usr/bin/env perl
+
+## This test will first cause a (legitimate) error in a
+## before_template_render hook
+## Then it will fetch an unrelated route that should return normally.
+## However, this route is now using the wrong with_return block.
+## This is because the first route, errors in rendering the *error* page.
+## This cause the block to die, and with_return is never unset.
+## This test uses two template files
+
+package MyTestApp;
+use Dancer2;
+
+hook before_template_render => sub {
+ my $path = request->path;
+ if ( $path =~ m!route_with_renderer_error! ) {
+ die session->id;
+ }
+};
+
+get '/route_with_renderer_error' => sub {
+ ## This route first gets called, then template fires the above hook.
+ ## This hook errors, causing Dancer2::Core::App, to throw an error
+ ## which *also* fires the hook, crashing the server.
+ session->write('bob' => "I SHOULD NOT BE IN THE NEXT SESSION");
+ my $tt = session->id;
+ template \"$tt";
+};
+
+get '/normal_route' => sub {
+ ## This should issue normally
+ if ( !session('bob') ) {
+ session('bob' => "test" . rand());
+ }
+ return session('bob');
+};
+
+package main;
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common qw/GET/;
+use Dancer2;
+
+my $test = Plack::Test->create(Dancer2->psgi_app);
+
+## This route works fine
+## Just a sanity check
+my $res1 = $test->request(GET '/normal_route');
+ok($res1->is_success, '/normal_route does not error');
+
+## This route should die and cause a broken state
+my $res2 = $test->request(GET '/route_with_renderer_error');
+ok(! $res2->is_success, '/route_with_renderer_error errors errors');
+
+## This route will now have the same session as the previous route.
+## Despite not having any cookies...
+my $res3 = $test->request(GET '/normal_route');
+ok($res3->is_success, '/normal_route does not error');
+my $session_value = $res3->decoded_content;
+isnt($session_value, "I SHOULD NOT BE IN THE NEXT SESSION",
+ '3rd route does not have session value from second route');
+
+done_testing();
diff --git a/t/scope_problems/views/500.tt b/t/scope_problems/views/500.tt
new file mode 100644
index 00000000..e16a9711
--- /dev/null
+++ b/t/scope_problems/views/500.tt
@@ -0,0 +1 @@
+This is a dummy error template
diff --git a/t/scope_problems/with_return_dies.t b/t/scope_problems/with_return_dies.t
new file mode 100644
index 00000000..2f6397f3
--- /dev/null
+++ b/t/scope_problems/with_return_dies.t
@@ -0,0 +1,72 @@
+#!/usr/bin/env perl
+
+## This test will first cause a (legitimate) error in a
+## before_template_render hook
+## Then it will fetch an unrelated route that should return normally.
+## However, this route is now using the wrong with_return block.
+## This is because the first route, errors in rendering the *error* page.
+## This cause the block to die, and with_return is never unset.
+## This test uses two template files
+
+package MyTestApp;
+use Dancer2;
+use Scalar::Util qw/refaddr/;
+
+hook before_template_render => sub {
+ my $path = request->path;
+ my $refadd = refaddr(app->with_return);
+ if ( $path =~ m!route_with_renderer_error! ) {
+ die $refadd;
+ }
+};
+
+get '/route_with_renderer_error' => sub {
+ ## This route first gets called, then template fires the above hook.
+ ## This hook errors, causing Dancer2::Core::App, to throw an error
+ ## which *also* fires the hook, crashing the server.
+ my $addr = refaddr(app->with_return);
+ template \$addr;
+};
+
+get '/normal_route' => sub {
+ ## This should issue normally
+ # my $addr = refaddr(app->with_return);
+ # template \$addr;
+ return refaddr(app->with_return);
+};
+
+package main;
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common qw/GET/;
+use Dancer2;
+
+my $test = Plack::Test->create(Dancer2->psgi_app);
+
+## This route works fine
+my $res1 = $test->request(GET '/normal_route');
+ok($res1->is_success, '/normal_route does not error');
+my $refaddr1 = $res1->decoded_content;
+
+## This route should die
+my $res2 = $test->request(GET '/route_with_renderer_error');
+ok(! $res2->is_success, '/route_with_renderer_error errors errors');
+my ($refaddr2) = $res2->decoded_content =~ /Hook error: (\d+)/;
+
+## The first route now errors
+## I can't seem to force with_return to fail in this test, even though I have
+## it failing in production.
+## So instead I'll check the refaddr of the with_return
+## If refaddr of with_return is the same between route2 and route3, then this
+## demonstrates that with_return has not been cleared between the two routes
+## And that /normal_route is now using the wrong with_return.
+## Possibly the old with_return hasn't been cleaned up? not sure.
+my $res3 = $test->request(GET '/normal_route');
+ok($res3->is_success, '/normal_route does not error');
+my $refaddr3 = $res3->decoded_content;
+isnt($refaddr1, $refaddr3, 'The 3rd request has a different with_return from the first run');
+isnt($refaddr2, $refaddr3, 'The 3rd request has a different with_return from the second run');
+
+done_testing();
diff --git a/t/serializer.t b/t/serializer.t
new file mode 100644
index 00000000..be76fc85
--- /dev/null
+++ b/t/serializer.t
@@ -0,0 +1,42 @@
+use strict;
+use warnings;
+
+use Test::More tests => 5;
+use Dancer2::Serializer::Dumper;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+{
+ 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',
+);
diff --git a/t/serializer_json.t b/t/serializer_json.t
new file mode 100644
index 00000000..c11e2bb1
--- /dev/null
+++ b/t/serializer_json.t
@@ -0,0 +1,75 @@
+use 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();
diff --git a/t/serializer_mutable.t b/t/serializer_mutable.t
new file mode 100644
index 00000000..89c19139
--- /dev/null
+++ b/t/serializer_mutable.t
@@ -0,0 +1,117 @@
+use 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<is_coderef>;
+
+{
+ package MyApp;
+ use Dancer2;
+ use Ref::Util qw<is_hashref>;
+
+ 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
diff --git a/t/serializer_mutable_custom.t b/t/serializer_mutable_custom.t
new file mode 100644
index 00000000..ff37ba08
--- /dev/null
+++ b/t/serializer_mutable_custom.t
@@ -0,0 +1,126 @@
+=pod
+
+Same as t/serializer_mutable.t, but exercise the configurable
+mappings
+
+=cut
+
+use 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<is_coderef>;
+
+{
+ package Dancer2::Serializer::Other;
+
+ use Moo;
+ with 'Dancer2::Core::Role::Serializer';
+
+ has '+content_type' => ( default => 'text/other' );
+
+ sub serialize { '{thing}' }
+ sub deserialize { '{thing}' }
+
+}
+
+{
+ package MyApp;
+ use Dancer2;
+
+ BEGIN {
+ setting engines => { serializer => { Mutable => { mapping => {
+ 'text/x-yaml' => 'YAML',
+ 'text/x-data-dumper' => 'Dumper',
+ 'text/x-json' => 'JSON',
+ 'application/json' => 'JSON',
+ 'text/other' => 'Other',
+ } } } };
+ }
+
+ use Ref::Util qw<is_hashref>;
+
+ 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 $app = MyApp->to_app;
+ok is_coderef($app), 'Got app';
+
+test_psgi $app, sub {
+ my $cb = shift;
+
+ # Configure all test cases
+ my $d = {
+ yaml => {
+ types => [ qw(text/x-yaml) ],
+ value => encode('UTF-8', YAML::Dump({ bar => 'baz' })),
+ last_val => "---bar:baz",
+ },
+ other => {
+ types => [ qw(text/other) ],
+ value => '{thing}',
+ last_val => "{thing}",
+ },
+ 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"}',
+ },
+ };
+
+ 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} } ) {
+ subtest $content_type => sub {
+ for my $ct (qw/Content-Type Accept/) {
+
+ # Test getting the value serialized in the correct format
+ my $res = $cb->( GET '/serialize', $ct => $content_type );
+
+ is( $res->code, 200, "status" );
+ is( $res->content, $s->{value}, "content" );
+ is(
+ $res->headers->content_type,
+ $content_type,
+ "content-type headers",
+ );
+ }
+
+ # Test sending the value serialized in the correct format
+ # needs to be de-serialized and returned
+ my $req = $cb->( POST '/deserialize',
+ 'Content-Type' => $content_type,
+ content => $s->{value} );
+
+ my $content = $req->content;
+ $content =~ s/\s//g;
+ is( $req->code, 200, "status" );
+ is( $content, $s->{last_val}, "content" );
+ }
+ }
+ }
+ }
+
+}
diff --git a/t/session_bad_client_cookie.t b/t/session_bad_client_cookie.t
new file mode 100644
index 00000000..7d223260
--- /dev/null
+++ b/t/session_bad_client_cookie.t
@@ -0,0 +1,119 @@
+use strict;
+use warnings;
+use Test::More;
+use File::Spec;
+use Plack::Test;
+use HTTP::Request::Common;
+use HTTP::Cookies;
+
+{
+ package
+ Dancer2::Session::SimpleHexId;
+ use Moo;
+ extends 'Dancer2::Session::Simple';
+
+ # Subclass that only generates and accepts 8 character hex session id's
+
+ our $valid_session_ids = 0;
+
+ sub generate_id {
+ return join "", map { sprintf("%x", rand 16) } 1..8
+ }
+
+ sub validate_id {
+ my $ok = ( $_[1] =~ m/^[0-9a-f]{8}$/ );
+ $valid_session_ids++ if $ok; # this should never increment in these tests
+ return $ok;
+ }
+}
+
+{
+
+ package App;
+ use Dancer2;
+
+ set engines => {
+ session => {
+ Simple => {cookie_name => 'dancer2.test'},
+ SimpleHexId => {cookie_name => 'dancer2.test'},
+ YAML => {cookie_name => 'dancer2.test'}
+ }
+ };
+
+ set session => 'Simple';
+ set show_errors => 1;
+
+ get '/set_session/*' => sub {
+ my ($name) = splat;
+ session name => $name;
+ };
+}
+
+my $url = 'http://localhost';
+my $test = Plack::Test->create(App->to_app);
+my $app = Dancer2->runner->apps->[0];
+
+my $bad_session_id = 'abcdefghijklmnopqrstuvwxyz123456';
+
+for my $engine (qw(YAML Simple SimpleHexId)) {
+
+ # 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;
+ my $session_engine = $app->session_engine; # trigger a build
+
+ if ($session_engine->can('session_dir')) {
+ # make sure our test file does not exist
+ my $bad_session_file =
+ File::Spec->catfile($session_engine->session_dir,
+ $bad_session_id . $session_engine->_suffix);
+ unlink $bad_session_file;
+ }
+
+ # run the tests for this engine
+
+ my $jar = HTTP::Cookies->new;
+
+ my @cookie;
+
+ subtest "[$engine] set_session and extract cookie" => sub {
+ my $res = $test->request(GET "$url/set_session/larry");
+ ok($res->is_success, "set_session");
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, 'Cookie set');
+
+ $jar->scan(sub { @cookie = @_ });
+ };
+
+ subtest "[$engine] set_session with bad cookie value" => sub {
+
+ # set session cookie value to something bad
+ $cookie[2] = $bad_session_id;
+ ok($jar->set_cookie(@cookie), "Set bad cookie value");
+
+ my $req = GET "$url/set_session/larry";
+ $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');
+
+ my $sid;
+ $jar->scan(sub { $sid = $_[2] });
+ isnt $sid, 'abcdefghijklmnopqrstuvwxyz123456',
+ "Session ID has been reset";
+ };
+}
+
+subtest "[SimpleHexId] valid session id count" => sub {
+ is $Dancer2::Session::SimpleHexId::valid_session_ids, 0,
+ "No valid session keys passed during test"
+};
+
+done_testing;
diff --git a/t/session_config.t b/t/session_config.t
new file mode 100644
index 00000000..8e99c9bd
--- /dev/null
+++ b/t/session_config.t
@@ -0,0 +1,86 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Cookies;
+use HTTP::Request::Common;
+
+{
+ package App;
+ use Dancer2;
+
+ setting(
+ engines => {
+ session => {
+ Simple => {
+ cookie_name => 'dancer.sid',
+ cookie_path => '/foo',
+ cookie_duration => '1 hour',
+ is_http_only => 0, # will not show up in cookie
+ },
+ },
+ }
+ );
+
+ setting( session => 'Simple' );
+
+ get '/has_session' => sub {
+ return app->has_session;
+ };
+
+ get '/foo/set_session/*' => sub {
+ my ($name) = splat;
+ session name => $name;
+ };
+
+ get '/foo/read_session' => sub {
+ my $name = session('name') || '';
+ "name='$name'";
+ };
+
+ get '/foo/destroy_session' => sub {
+ my $name = session('name') || '';
+ app->destroy_session;
+ return "destroyed='$name'";
+ };
+}
+
+my $test = Plack::Test->create( App->to_app );
+my $url = 'http://localhost';
+
+my $jar = HTTP::Cookies->new;
+
+subtest 'Set session' => sub {
+ my $res = $test->request( GET "$url/foo/set_session/larry" );
+ ok( $res->is_success, '/foo/set_session/larry' );
+
+ $jar->extract_cookies($res);
+ ok( $jar->as_string, 'session cookie set' );
+
+ my ( $expires, $domain, $path, $opts );
+ my $cookie = $jar->scan( sub {
+ ( $expires, $domain, $path, $opts ) = @_[ 8, 4, 3 ];
+ } );
+
+ my $httponly = $opts->{'HttpOnly'};
+
+ ok $expires - time > 3540,
+ "cookie expiration is in future";
+
+ is $domain, 'localhost.local', "cookie domain set";
+ is $path, '/foo', "cookie path set";
+ is $httponly, undef, "cookie has not set HttpOnly";
+
+ # read value back
+};
+
+subtest 'Read session' => sub {
+ my $req = GET "$url/foo/read_session";
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ ok $res->is_success, "/foo/read_session";
+ like $res->content, qr/name='larry'/, "session value looks good";
+};
+
+done_testing;
diff --git a/t/session_engines.t b/t/session_engines.t
new file mode 100644
index 00000000..d0ae52d5
--- /dev/null
+++ b/t/session_engines.t
@@ -0,0 +1,117 @@
+use strict;
+use warnings;
+use Test::More;
+use YAML;
+use Plack::Test;
+use HTTP::Cookies;
+use HTTP::Request::Common;
+
+use File::Spec;
+use File::Basename 'dirname';
+
+my $SESSION_DIR;
+BEGIN {
+ $SESSION_DIR = File::Spec->catfile( dirname(__FILE__), 'sessions' );
+}
+
+{
+ package App;
+ use Dancer2;
+ my @to_destroy;
+
+ set engines => { session => { YAML => { session_dir => $SESSION_DIR } } };
+
+ hook 'engine.session.before_destroy' => sub {
+ my $session = shift;
+ push @to_destroy, $session;
+ };
+
+ get '/set_session/*' => sub {
+ my ($name) = splat;
+ session name => $name;
+ };
+
+ get '/read_session' => sub {
+ my $name = session('name') || '';
+ "name='$name'";
+ };
+
+ get '/clear_session' => sub {
+ session name => undef;
+ return exists( session->data->{'name'} ) ? "failed" : "cleared";
+ };
+
+ get '/cleanup' => sub {
+ app->destroy_session;
+ return scalar(@to_destroy);
+ };
+
+ setting session => 'Simple';
+
+ set(
+ show_errors => 1,
+ environment => 'production',
+ );
+}
+
+my $url = "http://localhost";
+my $test = Plack::Test->create( App->to_app );
+my $app = Dancer2->runner->apps->[0];
+
+my @clients = qw(one two three);
+
+for my $engine ( qw(YAML Simple) ) {
+ # clear current session engine, and rebuild for the test
+ # This is *really* messy, playing in object hashrefs..
+ delete $app->{session_engine};
+ $app->config->{session} = $engine;
+ $app->session_engine; # trigger a build
+
+ # run the tests for this engine
+ for my $client (@clients) {
+ my $jar = HTTP::Cookies->new;
+
+ subtest "[$engine][$client] Empty session" => sub {
+ my $res = $test->request( GET "$url/read_session" );
+ like $res->content, qr/name=''/,
+ "empty session for client $client";
+ $jar->extract_cookies($res);
+ };
+
+ subtest "[$engine][$client] set_session" => sub {
+ my $req = GET "$url/set_session/$client";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ ok( $res->is_success, "set_session for client $client" );
+ $jar->extract_cookies($res);
+ };
+
+ subtest "[$engine][$client] session for client" => sub {
+ my $req = GET "$url/read_session";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ like $res->content, qr/name='$client'/,
+ "session looks good for client $client";
+ $jar->extract_cookies($res);
+ };
+
+ subtest "[$engine][$client] delete session" => sub {
+ my $req = GET "$url/clear_session";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ like $res->content, qr/cleared/, "deleted session key";
+ };
+
+ subtest "[$engine][$client] cleanup" => sub {
+ my $req = GET "$url/cleanup";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ ok( $res->is_success, "cleanup done for $client" );
+ ok( $res->content, "session hook triggered" );
+ };
+ }
+}
+
+
+
+done_testing;
diff --git a/t/session_forward.t b/t/session_forward.t
new file mode 100644
index 00000000..6136cd70
--- /dev/null
+++ b/t/session_forward.t
@@ -0,0 +1,195 @@
+use 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<foo bar baz>;
+ };
+
+ 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;
diff --git a/t/session_hooks.t b/t/session_hooks.t
new file mode 100644
index 00000000..701b156a
--- /dev/null
+++ b/t/session_hooks.t
@@ -0,0 +1,190 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Cookies;
+use HTTP::Request::Common;
+
+my @hooks_to_test = qw(
+ engine.session.before_retrieve
+ engine.session.after_retrieve
+
+ engine.session.before_create
+ engine.session.after_create
+
+ engine.session.before_change_id
+ engine.session.after_change_id
+
+ engine.session.before_destroy
+ engine.session.after_destroy
+
+ engine.session.before_flush
+ engine.session.after_flush
+);
+
+# we'll set a flag here when each hook is called. Then our test will then verify this
+my $test_flags = {};
+
+{
+ package App;
+ use Dancer2;
+
+ set(
+ show_errors => 1,
+ envoriment => 'production'
+ );
+
+ setting( session => 'Simple' );
+
+ for my $hook (@hooks_to_test) {
+ hook $hook => sub {
+ $test_flags->{$hook} ||= 0;
+ $test_flags->{$hook}++;
+ }
+ }
+
+ get '/set_session' => sub {
+ session foo => 'bar'; #setting causes a session flush
+ return "ok";
+ };
+
+ get '/get_session' => sub {
+ ::is session->read('foo'), 'bar', "Got the right session back";
+ return "ok";
+ };
+
+ get '/change_session_id' => sub {
+ app->change_session_id;
+ return "ok";
+ };
+
+ get '/destroy_session' => sub {
+ app->destroy_session;
+ return "ok";
+ };
+
+ #setup each hook again and test whether they return the correct type
+ #there is unfortunately quite some duplication here.
+ hook 'engine.session.before_create' => sub {
+ my ($response) = @_;
+ ::isa_ok( $response, 'Dancer2::Core::Session' );
+ };
+
+ hook 'engine.session.after_create' => sub {
+ my ($response) = @_;
+ ::isa_ok( $response, 'Dancer2::Core::Session' );
+ };
+
+ hook 'engine.session.after_retrieve' => sub {
+ my ($response) = @_;
+ ::isa_ok( $response, 'Dancer2::Core::Session' );
+ };
+}
+
+my $test = Plack::Test->create( App->to_app );
+my $jar = HTTP::Cookies->new;
+my $url = "http://localhost";
+
+is_deeply( $test_flags, {}, 'Make sure flag hash is clear' );
+
+subtest set_session => sub {
+ my $res = $test->request( GET "$url/set_session" );
+ is $res->content, "ok", "set_session ran ok";
+ $jar->extract_cookies($res);
+};
+
+# we verify whether the hooks were called correctly.
+subtest 'verify hooks for session create and session flush' => sub {
+ is $test_flags->{'engine.session.before_create'}, 1, "session.before_create called";
+ is $test_flags->{'engine.session.after_create'}, 1, "session.after_create called";
+ is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush called";
+ is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush called";
+
+ is $test_flags->{'engine.session.before_change_id'}, undef, "session.before_change_id not called";
+ is $test_flags->{'engine.session.after_change_id'}, undef, "session.after_change_id not called";
+ is $test_flags->{'engine.session.before_retrieve'}, undef, "session.before_retrieve not called";
+ is $test_flags->{'engine.session.after_retrieve'}, undef, "session.after_retrieve not called";
+ is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called";
+ is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called";
+};
+
+subtest 'verify Handler::File (static content) does not retrieve session' => sub {
+ my $req = GET "$url/file.txt";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ $jar->extract_cookies($res);
+
+ # These should not change from previous subtest
+ is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called";
+ is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called";
+ is $test_flags->{'engine.session.before_retrieve'}, undef, "session.before_retrieve not called";
+ is $test_flags->{'engine.session.after_retrieve'}, undef, "session.after_retrieve not called";
+};
+
+subtest get_session => sub {
+ my $req = GET "$url/get_session";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ is $res->content, "ok", "get_session ran ok";
+ $jar->extract_cookies($res);
+};
+
+subtest 'verify hooks for session retrieve' => sub {
+ is $test_flags->{'engine.session.before_retrieve'}, 1, "session.before_retrieve called";
+ is $test_flags->{'engine.session.after_retrieve'}, 1, "session.after_retrieve called";
+
+ is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called";
+ is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called";
+ is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called";
+ is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called";
+ is $test_flags->{'engine.session.before_change_id'}, undef, "session.before_change_id not called";
+ is $test_flags->{'engine.session.after_change_id'}, undef, "session.after_change_id not called";
+ is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called";
+ is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called";
+};
+
+subtest change_session_id => sub {
+ my $req = GET "$url/change_session_id";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ is $res->content, "ok", "get_session ran ok";
+ $jar->clear;
+ $jar->extract_cookies($res);
+};
+
+subtest 'verify hooks for change session id' => sub {
+ # change_session_id causes a retrieve
+ is $test_flags->{'engine.session.before_retrieve'}, 2, "session.before_retrieve called";
+ is $test_flags->{'engine.session.after_retrieve'}, 2, "session.after_retrieve called";
+
+ is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called";
+ is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called";
+ is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called";
+ is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called";
+ is $test_flags->{'engine.session.before_change_id'}, 1, "session.before_change_id called";
+ is $test_flags->{'engine.session.after_change_id'}, 1, "session.after_change_id called";
+ is $test_flags->{'engine.session.before_destroy'}, undef, "session.before_destroy not called";
+ is $test_flags->{'engine.session.after_destroy'}, undef, "session.after_destroy not called";
+};
+
+subtest destroy_session => sub {
+ my $req = GET "$url/destroy_session";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ is $res->content, "ok", "destroy_session ran ok";
+};
+
+subtest 'verify session destroy hooks' => sub {
+ is $test_flags->{'engine.session.before_destroy'}, 1, "session.before_destroy called";
+ is $test_flags->{'engine.session.after_destroy'}, 1, "session.after_destroy called";
+ #not sure if before and after retrieve should be called when the session is destroyed. But this happens.
+ is $test_flags->{'engine.session.before_retrieve'}, 3, "session.before_retrieve called";
+ is $test_flags->{'engine.session.after_retrieve'}, 3, "session.after_retrieve called";
+
+ is $test_flags->{'engine.session.before_create'}, 1, "session.before_create not called";
+ is $test_flags->{'engine.session.after_create'}, 1, "session.after_create not called";
+ is $test_flags->{'engine.session.before_flush'}, 1, "session.before_flush not called";
+ is $test_flags->{'engine.session.after_flush'}, 1, "session.after_flush not called";
+};
+
+done_testing;
diff --git a/t/session_hooks_no_change_id.t b/t/session_hooks_no_change_id.t
new file mode 100644
index 00000000..3a733a13
--- /dev/null
+++ b/t/session_hooks_no_change_id.t
@@ -0,0 +1,195 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Cookies;
+use HTTP::Request::Common;
+use lib 't/lib';
+
+my @hooks_to_test = qw(
+ engine.session.before_retrieve
+ engine.session.after_retrieve
+
+ engine.session.before_create
+ engine.session.after_create
+
+ engine.session.before_change_id
+ engine.session.after_change_id
+
+ engine.session.before_destroy
+ engine.session.after_destroy
+
+ engine.session.before_flush
+ engine.session.after_flush
+);
+
+# we'll set a flag here when each hook is called. Then our test will then verify this
+my $test_flags = {};
+
+{
+ package App;
+ use Dancer2;
+
+ set(
+ show_errors => 1,
+ envoriment => 'production'
+ );
+
+ setting( session => 'SimpleNoChangeId' );
+
+ 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";
+
+ # and a new session is created since this engine doesn't have _change_id
+ is $test_flags->{'engine.session.before_create'}, 2, "session.before_create not called";
+ is $test_flags->{'engine.session.after_create'}, 2, "session.after_create not called";
+ # flushed as well
+ is $test_flags->{'engine.session.before_flush'}, 2, "session.before_flush not called";
+ is $test_flags->{'engine.session.after_flush'}, 2, "session.after_flush not called";
+ # these should never be 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";
+ # and the old session was destroyed
+ is $test_flags->{'engine.session.before_destroy'}, 1, "session.before_destroy not called";
+ is $test_flags->{'engine.session.after_destroy'}, 1, "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'}, 2, "session.before_destroy called";
+ is $test_flags->{'engine.session.after_destroy'}, 2, "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'}, 2, "session.before_create not called";
+ is $test_flags->{'engine.session.after_create'}, 2, "session.after_create not called";
+ is $test_flags->{'engine.session.before_flush'}, 2, "session.before_flush not called";
+ is $test_flags->{'engine.session.after_flush'}, 2, "session.after_flush not called";
+};
+
+done_testing;
diff --git a/t/session_in_template.t b/t/session_in_template.t
new file mode 100644
index 00000000..0979e4db
--- /dev/null
+++ b/t/session_in_template.t
@@ -0,0 +1,98 @@
+use strict;
+use warnings;
+use Test::More;
+
+use Plack::Test;
+use HTTP::Request::Common;
+use HTTP::Cookies;
+use Ref::Util qw<is_coderef>;
+
+{
+ package TestApp;
+
+ use Dancer2;
+
+ get '/' => sub {
+ template 'session_in_template'
+ };
+
+ get '/set_session/*' => sub {
+ my ($name) = splat;
+ session name => $name;
+ template 'session_in_template';
+ };
+
+ get '/destroy_session' => sub {
+ # Need to call the 'session' keyword, so app->setup_session
+ # is called and the session attribute in the engines is populated
+ my $name = session 'name';
+ # Destroying the session should remove the session object from
+ # all engines.
+ app->destroy_session;
+ template 'session_in_template';
+ };
+
+ setting(
+ engines => {
+ session => { 'Simple' => { session_dir => 't/sessions' } }
+ }
+ );
+ setting( session => 'Simple' );
+}
+
+my $app = TestApp->to_app;
+ok( is_coderef($app), 'Got app' );
+
+my $test = Plack::Test->create($app);
+my $jar = HTTP::Cookies->new();
+my $base = 'http://localhost';
+
+{
+ my $res = $test->request( GET "$base/" );
+
+ ok $res->is_success, 'Successful request';
+ is $res->content, "session.name \n";
+
+ $jar->extract_cookies($res);
+}
+
+{
+ my @requests = (
+ GET("$base/set_session/test_name"),
+ GET("$base/")
+ );
+ for my $req ( @requests ) {
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ ok $res->is_success, 'Successful request';
+ is $res->content, "session.name test_name\n";
+
+ $jar->extract_cookies($res);
+ }
+}
+
+{
+ my $request = GET "$base/";
+ $jar->add_cookie_header($request);
+
+ my $res = $test->request($request);
+ ok $res->is_success, 'Successful request';
+ is $res->content, "session.name test_name\n";
+
+ $jar->extract_cookies($res);
+}
+
+
+{
+ my $request = GET "$base/destroy_session";
+ $jar->add_cookie_header($request);
+
+ my $res = $test->request($request);
+ ok $res->is_success, 'Successful request';
+ is $res->content, "session.name \n";
+
+ $jar->extract_cookies($res);
+}
+
+done_testing();
diff --git a/t/session_lifecycle.t b/t/session_lifecycle.t
new file mode 100644
index 00000000..644e6cc9
--- /dev/null
+++ b/t/session_lifecycle.t
@@ -0,0 +1,227 @@
+use strict;
+use warnings;
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use HTTP::Cookies;
+use lib 't/lib';
+
+{
+ package App;
+ use Dancer2;
+
+ set session => 'Simple';
+ set show_errors => 1;
+
+ get '/no_session_data' => sub {
+ return "session not modified";
+ };
+
+ get '/set_session/*' => sub {
+ my ($name) = splat;
+ session name => $name;
+ };
+
+ get '/read_session' => sub {
+ my $name = session('name') || '';
+ "name='$name'";
+ };
+
+ get '/change_session_id' => sub {
+ app->change_session_id;
+ };
+
+ get '/destroy_session' => sub {
+ my $name = session('name') || '';
+ app->destroy_session;
+ return "destroyed='$name'";
+ };
+
+ get '/churn_session' => sub {
+ app->destroy_session;
+ session name => 'damian';
+ return "churned";
+ };
+}
+
+my $url = 'http://localhost';
+my $test = Plack::Test->create( App->to_app );
+my $app = Dancer2->runner->apps->[0];
+
+for my $engine (qw(YAML Simple SimpleNoChangeId)) {
+
+ # clear current session engine, and rebuild for the test
+ # This is *really* messy, playing in object hashrefs..
+ delete $app->{session_engine};
+ $app->config->{session} = $engine;
+ $app->session_engine; # trigger a build
+
+ my $jar = HTTP::Cookies->new();
+
+ subtest "[$engine] No cookie set if session not referenced" => sub {
+ my $res = $test->request(GET "$url/no_session_data");
+ ok $res->is_success, "/no_session_data"
+ or diag explain $res;
+
+ $jar->extract_cookies($res);
+ ok(!$jar->as_string, 'No cookie set');
+ };
+
+ subtest "[$engine] No empty session created if session read attempted" =>
+ sub {
+ my $res = $test->request(GET "$url/read_session");
+ ok $res->is_success, "/read_session";
+
+ $jar->extract_cookies($res);
+ ok(!$jar->as_string, 'No cookie set');
+ };
+
+ my $sid1;
+ subtest "[$engine] Set value into session" => sub {
+ my $res = $test->request(GET "$url/set_session/larry");
+ ok $res->is_success, "/set_session/larry";
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, 'Cookie set');
+
+ # extract SID
+ $jar->scan(sub { $sid1 = $_[2] });
+ ok($sid1, 'Got SID from cookie');
+ };
+
+ subtest "[$engine] Read value back" => sub {
+
+ # read value back
+ my $req = GET "$url/read_session";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ ok $res->is_success, "/read_session";
+
+ $jar->clear;
+ ok(!$jar->as_string, 'Jar cleared');
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, 'session cookie set again');
+ like $res->content, qr/name='larry'/, "session value looks good";
+ };
+
+ subtest
+ "[$engine] Session cookie persists even if we do not touch sessions" =>
+ sub {
+ my $req = GET "$url/no_session_data";
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ ok $res->is_success, "/no_session_data";
+
+ $jar->clear;
+ ok(!$jar->as_string, 'Jar cleared');
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, 'session cookie set again');
+ };
+
+ my $sid2;
+ subtest "[$engine] Change session ID" => sub {
+ my $req = GET "$url/change_session_id";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ ok $res->is_success, "/change_session_id";
+
+ $jar->clear;
+ ok(!$jar->as_string, 'Jar cleared');
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, 'session cookie set again');
+
+ # extract SID
+ $jar->scan(sub { $sid2 = $_[2] });
+ isnt $sid2, $sid1, "New session has different ID";
+ is $res->content, $sid2, "new session ID returned";
+ };
+
+ subtest "[$engine] Read value back after change_session_id" => sub {
+
+ # read value back
+ my $req = GET "$url/read_session";
+ $jar->add_cookie_header($req);
+ my $res = $test->request($req);
+ ok $res->is_success, "/read_session";
+
+ $jar->clear;
+ ok(!$jar->as_string, 'Jar cleared');
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, 'session cookie set again');
+ like $res->content, qr/name='larry'/, "session value looks good";
+ };
+
+ subtest
+ "[$engine] Destroy session and check that cookies expiration is set" =>
+ sub {
+ my $req = GET "$url/destroy_session";
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ ok $res->is_success, "/destroy_session";
+
+ ok($jar->as_string, 'We have a cookie before reading response');
+ $jar->extract_cookies($res);
+ ok(!$jar->as_string, 'Cookie was removed from jar');
+ };
+
+ subtest "[$engine] Session cookie not sent after session destruction" =>
+ sub {
+ my $req = GET "$url/no_session_data";
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ ok $res->is_success, "/no_session_data";
+
+ ok(!$jar->as_string, 'Jar is empty');
+ $jar->extract_cookies($res);
+ ok(!$jar->as_string, 'Jar still empty (no new session cookie)');
+ };
+
+ my $sid3;
+ subtest "[$engine] Set value into session again" => sub {
+ my $res = $test->request(GET "$url/set_session/curly");
+ ok $res->is_success, "/set_session/larry";
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, 'session cookie set');
+
+ # extract SID
+ $jar->scan(sub { $sid3 = $_[2] });
+ isnt $sid3, $sid2, "New session has different ID";
+ };
+
+ subtest "[$engine] Destroy and create a session in one request" => sub {
+ my $req = GET "$url/churn_session";
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ ok $res->is_success, "/churn_session";
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, 'session cookie set');
+
+ my $sid4;
+ $jar->scan(sub { $sid4 = $_[2] });
+ isnt $sid4, $sid3, "Changed session has different ID";
+ };
+
+ subtest "[$engine] Read value back" => sub {
+ my $req = GET "$url/read_session";
+ $jar->add_cookie_header($req);
+
+ my $res = $test->request($req);
+ ok $res->is_success, "/read_session";
+
+ $jar->extract_cookies($res);
+ ok($jar->as_string, "session cookie set");
+ like $res->content, qr/name='damian'/, "session value looks good";
+ };
+}
+
+done_testing;
diff --git a/t/session_object.t b/t/session_object.t
new file mode 100644
index 00000000..1263158a
--- /dev/null
+++ b/t/session_object.t
@@ -0,0 +1,50 @@
+# 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;
diff --git a/t/shared_engines.t b/t/shared_engines.t
new file mode 100644
index 00000000..00c4226e
--- /dev/null
+++ b/t/shared_engines.t
@@ -0,0 +1,51 @@
+use 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;
diff --git a/t/static_content.t b/t/static_content.t
new file mode 100644
index 00000000..a31cd3ea
--- /dev/null
+++ b/t/static_content.t
@@ -0,0 +1,30 @@
+use 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();
diff --git a/t/template.t b/t/template.t
new file mode 100644
index 00000000..e75ccad1
--- /dev/null
+++ b/t/template.t
@@ -0,0 +1,199 @@
+use strict;
+use warnings;
+use Test::More;
+use Dancer2::Core::Hook;
+use Plack::Test;
+use HTTP::Request::Common;
+
+use File::Spec;
+use File::Basename 'dirname';
+
+eval { require Template; Template->import(); 1 }
+ or plan skip_all => 'Template::Toolkit probably missing.';
+
+use_ok('Dancer2::Template::TemplateToolkit');
+
+my $views =
+ File::Spec->rel2abs( File::Spec->catfile( dirname(__FILE__), 'views' ) );
+
+my $tt = Dancer2::Template::TemplateToolkit->new(
+ views => $views,
+ layout => 'main.tt',
+ 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';
+};
+
+{
+
+ 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;
diff --git a/t/template_default_tokens.t b/t/template_default_tokens.t
new file mode 100644
index 00000000..b298a0a1
--- /dev/null
+++ b/t/template_default_tokens.t
@@ -0,0 +1,55 @@
+use strict;
+use warnings;
+use File::Spec;
+use File::Basename 'dirname';
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+eval { require Template; Template->import(); 1 }
+ or plan skip_all => 'Template::Toolkit probably missing.';
+
+my $views =
+ File::Spec->rel2abs( File::Spec->catfile( dirname(__FILE__), 'views' ) );
+
+{
+
+ package Foo;
+
+ use Dancer2;
+ set session => 'Simple';
+
+ set views => $views;
+ set template => "template_toolkit";
+ set foo => "in settings";
+
+ get '/view/:foo' => sub {
+ var foo => "in var";
+ session foo => "in session";
+ template "tokens";
+ };
+}
+
+my $version = Dancer2->VERSION;
+my $expected = "perl_version: $^V
+dancer_version: ${version}
+settings.foo: in settings
+params.foo: 42
+session.foo in session
+vars.foo: in var";
+
+my $app = Foo->to_app;
+ok( is_coderef($app), 'Got app' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+
+ like(
+ $cb->( GET '/view/42' )->content,
+ qr{$expected},
+ 'Response contains all expected tokens',
+ );
+};
+
+done_testing;
diff --git a/t/template_ext.t b/t/template_ext.t
new file mode 100644
index 00000000..4fce202d
--- /dev/null
+++ b/t/template_ext.t
@@ -0,0 +1,29 @@
+use 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;
diff --git a/t/template_name.t b/t/template_name.t
new file mode 100644
index 00000000..83ce8b75
--- /dev/null
+++ b/t/template_name.t
@@ -0,0 +1,29 @@
+use strict;
+use warnings;
+use File::Spec;
+use File::Basename 'dirname';
+use Test::More;
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+{
+
+ 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;
diff --git a/t/template_simple.t b/t/template_simple.t
new file mode 100644
index 00000000..85436a5e
--- /dev/null
+++ b/t/template_simple.t
@@ -0,0 +1,89 @@
+use Test::More tests => 9;
+
+use strict;
+use warnings;
+use Dancer2::FileUtils 'path';
+
+use Dancer2::Template::Simple;
+
+{
+ package Foo;
+ use Moo;
+
+ has x => ( is => 'rw');
+ has y => ( is => 'rw');
+
+ sub method { "yeah" }
+}
+
+# variable interpolation, with file-based template
+
+my $engine = Dancer2::Template::Simple->new;
+my $template = path('t', 'views', 'template_simple_index.tt');
+
+my $result = $engine->render(
+ $template,
+ { var1 => "xxx",
+ var2 => "yyy",
+ foo => 'one',
+ bar => 'two',
+ baz => 'three'});
+
+my $expected = 'this is var1="xxx" and var2=yyy'."\n\nanother line\n\n one two three\n\nxxx/xxx\n";
+is $result, $expected, "template got processed successfully";
+
+# variable interpolation, with scalar-based template
+
+$expected = "one=1, two=2, three=3 - 77";
+$template = "one=<% one %>, two=<% two %>, three=<% three %> - <% hash.key %>";
+
+eval { $engine->render($template, { one => 1, two => 2, three => 3}) };
+like $@, qr/Can't open .* using mode '<'/, "prototype failure detected";
+
+$result = $engine->render(\$template, {
+ one => 1, two => 2, three => 3,
+ hash => { key => 77 },
+});
+is $result, $expected, "processed a template given as a scalar ref";
+
+# complex variable interpolation (object, coderef and hash)
+
+my $foo = Foo->new;
+$foo->x(42);
+
+$template = 'foo->x == <% foo.x %> foo.method == <% foo.method %> foo.dumb=\'<% foo.dumb %>\'';
+$expected = 'foo->x == 42 foo.method == yeah foo.dumb=\'\'';
+$result = $engine->render(\$template, { foo => $foo });
+is $result, $expected, 'object are interpolated in templates';
+
+$template = 'code = <% code %>, code <% hash.code %>';
+$expected = 'code = 42, code 42';
+$result = $engine->render(\$template, {
+ code => sub { 42 },
+ hash => {
+ code => sub { 42 }
+ }
+ });
+is $result, $expected, 'code ref are interpolated in templates';
+
+$template = 'array: <% array %>, hash.array: <% hash.array %>';
+$expected = 'array: 1 2 3 4 5, hash.array: 6 7 8';
+$result = $engine->render(\$template, {
+ array => [1, 2, 3, 4, 5],
+ hash => { array => [6, 7, 8] }});
+is $result, $expected, "arrayref are interpolated in templates";
+
+# if-then-else
+$template = '<% if want %>hello<% else %>goodbye<% end %> world';
+$result = $engine->render(\$template, {want => 1});
+is $result, 'hello world', "true boolean condition matched";
+$result = $engine->render(\$template, {want => 0});
+is $result, 'goodbye world', "false boolean condition matched";
+
+$template = 'one: 1
+two: <% two %>
+three : <% three %>';
+$result = $engine->render(\$template, {two => 2, three => 3 });
+is $result, 'one: 1
+two: 2
+three : 3', "multiline template processed";
diff --git a/t/template_tiny/01_compile.t b/t/template_tiny/01_compile.t
new file mode 100644
index 00000000..c167e7e2
--- /dev/null
+++ b/t/template_tiny/01_compile.t
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+
+use strict;
+
+BEGIN {
+ $| = 1;
+ $^W = 1;
+}
+use Test::More tests => 1;
+
+use_ok('Dancer2::Template::Implementation::ForkedTiny');
diff --git a/t/template_tiny/02_trivial.t b/t/template_tiny/02_trivial.t
new file mode 100644
index 00000000..0c81b6ef
--- /dev/null
+++ b/t/template_tiny/02_trivial.t
@@ -0,0 +1,34 @@
+#!/usr/bin/env perl
+
+use strict;
+
+BEGIN {
+ $| = 1;
+ $^W = 1;
+}
+use Test::More tests => 1;
+use Dancer2::Template::Implementation::ForkedTiny ();
+
+sub process {
+ my $stash = shift;
+ my $input = shift;
+ my $expected = shift;
+ my $message = shift || 'Template processed ok';
+ my $output = '';
+ Dancer2::Template::Implementation::ForkedTiny->new->process(
+ \$input,
+ $stash, \$output
+ );
+ is( $output, $expected, $message );
+}
+
+
+######################################################################
+# Main Tests
+
+process( { foo => 'World' },
+ <<'END_TEMPLATE', <<'END_EXPECTED', 'Trivial ok' );
+Hello [% foo %]!
+END_TEMPLATE
+Hello World!
+END_EXPECTED
diff --git a/t/template_tiny/03_samples.t b/t/template_tiny/03_samples.t
new file mode 100644
index 00000000..30a8c66c
--- /dev/null
+++ b/t/template_tiny/03_samples.t
@@ -0,0 +1,110 @@
+#!/usr/bin/env perl
+
+use strict;
+
+BEGIN {
+ $| = 1;
+ $^W = 1;
+}
+use vars qw{$VAR1 $VAR2};
+use Test::More;
+use File::Spec::Functions ':ALL';
+use Dancer2::Template::Implementation::ForkedTiny ();
+use FindBin qw($Bin);
+use Ref::Util qw<is_hashref>;
+
+my $SAMPLES = catdir( $Bin, 'samples' );
+unless ( -d $SAMPLES ) {
+ die("Failed to find samples directory");
+}
+
+opendir( DIR, $SAMPLES ) or die("opendir($SAMPLES): $!");
+my @TEMPLATES = sort grep {/\.tt$/} readdir(DIR);
+closedir(DIR) or die("closedir($SAMPLES): $!");
+
+plan( tests => scalar(@TEMPLATES) * 6 );
+
+# Test the test classes
+#SCOPE: {
+# my $false = bless { }, 'False';
+# my $string = $false . '';
+# is( $string, 'Hello', 'False objects return ok as a string' );
+# is( !!$false, '', 'False objects returns false during bool' );
+#}
+
+
+######################################################################
+# Main Tests
+
+foreach my $template (@TEMPLATES) {
+ $template =~ s/\.tt$//;
+ my $file = catfile( $SAMPLES, $template );
+ my $tt_file = "$file.tt";
+ my $var_file = "$file.var";
+ my $txt_file = "$file.txt";
+ ok( -f $tt_file, "$template: Found $tt_file" );
+ ok( -f $txt_file, "$template: Found $txt_file" );
+ ok( -f $var_file, "$template: Found $var_file" );
+
+ # Load the resources
+ my $tt = slurp($tt_file);
+ my $var = slurp($var_file);
+ my $txt = slurp($txt_file);
+ eval $var;
+ die $@ if $@;
+ ok( is_hashref($VAR1), "$template: Loaded stash from file" );
+
+ # Create the processor normally
+ my %params = ( INCLUDE_PATH => $SAMPLES, );
+ %params = ( %params, %$VAR2 ) if $VAR2;
+ my $template = Dancer2::Template::Implementation::ForkedTiny->new(%params);
+ isa_ok( $template, 'Dancer2::Template::Implementation::ForkedTiny' );
+
+ # Execute the template
+ $template->process( \$tt, $VAR1, \my $out );
+ is( $out, $txt, "$template: Output matches expected" );
+}
+
+sub slurp {
+ my $f = shift;
+ local $/ = undef;
+ open( VAR, $f ) or die("open($f): $!");
+ my $buffer = <VAR>;
+ close VAR;
+ return $buffer;
+}
+
+
+######################################################################
+# Support Classes for object tests
+
+SCOPE: {
+
+ package UpperCase;
+
+ sub foo {
+ uc $_[0]->{foo};
+ }
+
+ 1;
+}
+
+SCOPE: {
+
+ package False;
+
+ use overload 'bool' => sub {0};
+ use overload '""' => sub {'Hello'};
+
+ 1;
+}
+
+SCOPE: {
+
+ package Private;
+
+ sub public {'foo'}
+ sub _private {'foo'}
+
+ 1;
+}
diff --git a/t/template_tiny/04_compat.t b/t/template_tiny/04_compat.t
new file mode 100644
index 00000000..b81cc992
--- /dev/null
+++ b/t/template_tiny/04_compat.t
@@ -0,0 +1,107 @@
+#!/usr/bin/env perl
+
+use strict;
+
+BEGIN {
+ $| = 1;
+ $^W = 1;
+}
+use vars qw{$VAR1 $VAR2};
+use Test::More;
+use File::Spec::Functions ':ALL';
+eval "require Template";
+if ($@) {
+ plan( skip_all => 'Template Toolkit is not installed' );
+}
+use Ref::Util qw<is_hashref>;
+use FindBin qw($Bin);
+my $SAMPLES = catdir( $Bin, 'samples' );
+unless ( -d $SAMPLES ) {
+ die("Failed to find samples directory");
+}
+
+opendir( DIR, $SAMPLES ) or die("opendir($SAMPLES): $!");
+my @TEMPLATES = sort grep {/\.tt$/} readdir(DIR);
+closedir(DIR) or die("closedir($SAMPLES): $!");
+
+plan( tests => scalar(@TEMPLATES) * 7 );
+
+
+######################################################################
+# Main Tests
+
+foreach my $name (@TEMPLATES) {
+ $name =~ s/\.tt$//;
+ my $file = catfile( $SAMPLES, $name );
+ my $tt_file = "$file.tt";
+ my $var_file = "$file.var";
+ my $txt_file = "$file.txt";
+ ok( -f $tt_file, "$name: Found $tt_file" );
+ ok( -f $txt_file, "$name: Found $txt_file" );
+ ok( -f $var_file, "$name: Found $var_file" );
+
+ # Load the resources
+ my $tt = slurp($tt_file);
+ my $var = slurp($var_file);
+ my $txt = slurp($txt_file);
+ eval $var;
+ die $@ if $@;
+ ok( is_hashref($VAR1), "$name: Loaded stash from file" );
+
+ # Create the template processor
+ my %params = ( INCLUDE_PATH => $SAMPLES, );
+ %params = ( %params, %$VAR2 ) if $VAR2;
+ my $template = Template->new(%params);
+ isa_ok( $template, 'Template' );
+
+ # Execute the template
+ my $out = '';
+ ok( $template->process( \$tt, $VAR1, \$out ),
+ "$name: ->process returns true"
+ );
+ is( $out, $txt, "$name: Output matches expected" );
+}
+
+sub slurp {
+ my $f = shift;
+ local $/ = undef;
+ open( VAR, $f ) or die("open($f): $!");
+ my $buffer = <VAR>;
+ close VAR;
+ return $buffer;
+}
+
+
+######################################################################
+# Support Classes for object tests
+
+SCOPE: {
+
+ package UpperCase;
+
+ sub foo {
+ uc $_[0]->{foo};
+ }
+
+ 1;
+}
+
+SCOPE: {
+
+ package False;
+
+ use overload 'bool' => sub {0};
+ use overload '""' => sub {'Hello'};
+
+ 1;
+}
+
+SCOPE: {
+
+ package Private;
+
+ sub public {'foo'}
+ sub _private {'foo'}
+
+ 1;
+}
diff --git a/t/template_tiny/05_preparse.t b/t/template_tiny/05_preparse.t
new file mode 100644
index 00000000..51c2c36c
--- /dev/null
+++ b/t/template_tiny/05_preparse.t
@@ -0,0 +1,69 @@
+#!/usr/bin/env perl
+
+use strict;
+
+BEGIN {
+ $| = 1;
+ $^W = 1;
+}
+use Test::More tests => 6;
+use Dancer2::Template::Implementation::ForkedTiny ();
+
+sub preprocess {
+ my $template = $_[0];
+ my $expected = $_[1];
+ my $message = $_[2] || 'Template preprocessd ok';
+ my $prepared =
+ Dancer2::Template::Implementation::ForkedTiny->new->preprocess(
+ $template);
+ is( $prepared, $expected, $message );
+ is( $template, $_[0],
+ '->proprocess does not modify original template variable'
+ );
+}
+
+
+######################################################################
+# Main Tests
+
+preprocess( <<'END_TEMPLATE', <<'END_EXPECTED', 'Simple IF' );
+foo
+[% IF foo %]
+foobar
+[% END %]
+bar
+END_TEMPLATE
+foo
+[% I1 foo %]
+foobar
+[% I1 %]
+bar
+END_EXPECTED
+
+preprocess( <<'END_TEMPLATE', <<'END_EXPECTED', 'Simple UNLESS' );
+foo
+[% UNLESS foo %]
+foobar
+[% END %]
+bar
+END_TEMPLATE
+foo
+[% U1 foo %]
+foobar
+[% U1 %]
+bar
+END_EXPECTED
+
+preprocess( <<'END_TEMPLATE', <<'END_EXPECTED', 'Simple FOREACH' );
+foo
+[% FOREACH element IN lists %]
+foobar
+[% END %]
+bar
+END_TEMPLATE
+foo
+[% F1 element IN lists %]
+foobar
+[% F1 %]
+bar
+END_EXPECTED
diff --git a/t/template_tiny/samples/01_hello.tt b/t/template_tiny/samples/01_hello.tt
new file mode 100644
index 00000000..e91b5288
--- /dev/null
+++ b/t/template_tiny/samples/01_hello.tt
@@ -0,0 +1,8 @@
+Hello [% foo %]!
+Hello [%foo%]!
+Hello [% foo %]!
+Hello [%
+foo
+%]!
+Hello
+[% foo %]!
diff --git a/t/template_tiny/samples/01_hello.txt b/t/template_tiny/samples/01_hello.txt
new file mode 100644
index 00000000..831dc24b
--- /dev/null
+++ b/t/template_tiny/samples/01_hello.txt
@@ -0,0 +1,6 @@
+Hello World!
+Hello World!
+Hello World!
+Hello World!
+Hello
+World!
diff --git a/t/template_tiny/samples/01_hello.var b/t/template_tiny/samples/01_hello.var
new file mode 100644
index 00000000..ebf5ab24
--- /dev/null
+++ b/t/template_tiny/samples/01_hello.var
@@ -0,0 +1,3 @@
+$VAR1 = {
+ 'foo' => 'World'
+ };
diff --git a/t/template_tiny/samples/02_null.tt b/t/template_tiny/samples/02_null.tt
new file mode 100644
index 00000000..a068237b
--- /dev/null
+++ b/t/template_tiny/samples/02_null.tt
@@ -0,0 +1 @@
+a[% foo %]b[% bar %]c[% foo %]d
diff --git a/t/template_tiny/samples/02_null.txt b/t/template_tiny/samples/02_null.txt
new file mode 100644
index 00000000..acbe86c7
--- /dev/null
+++ b/t/template_tiny/samples/02_null.txt
@@ -0,0 +1 @@
+abcd
diff --git a/t/template_tiny/samples/02_null.var b/t/template_tiny/samples/02_null.var
new file mode 100644
index 00000000..d41ea8c5
--- /dev/null
+++ b/t/template_tiny/samples/02_null.var
@@ -0,0 +1,3 @@
+$VAR1 = {
+ 'foo' => '',
+ };
diff --git a/t/template_tiny/samples/03_chomp.tt b/t/template_tiny/samples/03_chomp.tt
new file mode 100644
index 00000000..07fe097e
--- /dev/null
+++ b/t/template_tiny/samples/03_chomp.tt
@@ -0,0 +1,16 @@
+foo[%- a -%]bar
+foo [%- a -%] bar
+foo
+[% a %]
+bar
+foo
+
+ [%- a %]
+bar
+foo
+[% a -%]
+
+ bar
+foo
+[%- a -%]
+bar
diff --git a/t/template_tiny/samples/03_chomp.txt b/t/template_tiny/samples/03_chomp.txt
new file mode 100644
index 00000000..030b3ffb
--- /dev/null
+++ b/t/template_tiny/samples/03_chomp.txt
@@ -0,0 +1,12 @@
+foobar
+foo bar
+foo
+
+bar
+foo
+
+bar
+foo
+
+ bar
+foobar
diff --git a/t/template_tiny/samples/03_chomp.var b/t/template_tiny/samples/03_chomp.var
new file mode 100644
index 00000000..90f61a9c
--- /dev/null
+++ b/t/template_tiny/samples/03_chomp.var
@@ -0,0 +1 @@
+$VAR1 = { };
diff --git a/t/template_tiny/samples/04_nested.tt b/t/template_tiny/samples/04_nested.tt
new file mode 100644
index 00000000..ca17d502
--- /dev/null
+++ b/t/template_tiny/samples/04_nested.tt
@@ -0,0 +1,9 @@
+[% a %]
+[% foo.one %]
+[% foo.two %]
+[% foo.three.0 %]
+[% foo.three.1 %]
+[% foo.three.2 %]
+[% foo._private %]
+[% bar.0.foo %]
+[% bar.bad %]
diff --git a/t/template_tiny/samples/04_nested.txt b/t/template_tiny/samples/04_nested.txt
new file mode 100644
index 00000000..e82d2414
--- /dev/null
+++ b/t/template_tiny/samples/04_nested.txt
@@ -0,0 +1,9 @@
+b
+1
+2
+1
+4
+9
+
+bar
+
diff --git a/t/template_tiny/samples/04_nested.var b/t/template_tiny/samples/04_nested.var
new file mode 100644
index 00000000..633b42bc
--- /dev/null
+++ b/t/template_tiny/samples/04_nested.var
@@ -0,0 +1,14 @@
+$VAR1 = {
+ a => 'b',
+ foo => {
+ one => 1,
+ two => 2,
+ three => [ 1, 4, 9 ],
+ _private => 'secret',
+ },
+ bar => [
+ {
+ foo => 'bar',
+ },
+ ],
+};
diff --git a/t/template_tiny/samples/05_condition.tt b/t/template_tiny/samples/05_condition.tt
new file mode 100644
index 00000000..2a74d091
--- /dev/null
+++ b/t/template_tiny/samples/05_condition.tt
@@ -0,0 +1,14 @@
+ [%- IF foo %]World[% END %]!
+Hello[% IF bar %] World[% END %]!
+Hello[% UNLESS foo %] World[% END %]!
+Hello[% UNLESS bar %] World[% END %]!
+[% IF foo -%]
+foo
+[%- ELSE -%]
+bar
+[%- END -%]
+[% IF bar %]
+BAR
+[% ELSE %]
+FOO
+[% END -%]
diff --git a/t/template_tiny/samples/05_condition.txt b/t/template_tiny/samples/05_condition.txt
new file mode 100644
index 00000000..7b9a9511
--- /dev/null
+++ b/t/template_tiny/samples/05_condition.txt
@@ -0,0 +1,6 @@
+World!
+Hello!
+Hello!
+Hello World!
+foo
+FOO
diff --git a/t/template_tiny/samples/05_condition.var b/t/template_tiny/samples/05_condition.var
new file mode 100644
index 00000000..b07e4802
--- /dev/null
+++ b/t/template_tiny/samples/05_condition.var
@@ -0,0 +1,4 @@
+$VAR1 = {
+ 'foo' => 1,
+ 'bar' => 0,
+ };
diff --git a/t/template_tiny/samples/06_object.tt b/t/template_tiny/samples/06_object.tt
new file mode 100644
index 00000000..371c535e
--- /dev/null
+++ b/t/template_tiny/samples/06_object.tt
@@ -0,0 +1,5 @@
+[% foo.foo %]
+[% bar %]
+[% IF bar %]true[% ELSE %]false[% END %]
+public = '[% baz.public %]'
+private = '[% baz._private %]'
diff --git a/t/template_tiny/samples/06_object.txt b/t/template_tiny/samples/06_object.txt
new file mode 100644
index 00000000..dc621732
--- /dev/null
+++ b/t/template_tiny/samples/06_object.txt
@@ -0,0 +1,5 @@
+BAR
+Hello
+false
+public = 'foo'
+private = ''
diff --git a/t/template_tiny/samples/06_object.var b/t/template_tiny/samples/06_object.var
new file mode 100644
index 00000000..22cfc77b
--- /dev/null
+++ b/t/template_tiny/samples/06_object.var
@@ -0,0 +1,5 @@
+$VAR1 = {
+ foo => bless( { foo => 'bar' }, 'UpperCase' ),
+ bar => bless( { }, 'False' ),
+ baz => bless( { }, 'Private' ),
+};
diff --git a/t/template_tiny/samples/07_nesting.tt b/t/template_tiny/samples/07_nesting.tt
new file mode 100644
index 00000000..4142817f
--- /dev/null
+++ b/t/template_tiny/samples/07_nesting.tt
@@ -0,0 +1,20 @@
+[% IF true %]
+ one
+[% END %]
+[% IF false %]
+ two
+[% END %]
+[% IF true %]
+ [% IF true %]
+ three
+ [% END %]
+ [% IF false %]
+ four
+ [% END %]
+[% END %]
+[% IF true %]
+ five
+[% END %]
+[% IF false %]
+ six
+[% END %] \ No newline at end of file
diff --git a/t/template_tiny/samples/07_nesting.txt b/t/template_tiny/samples/07_nesting.txt
new file mode 100644
index 00000000..a040bd9d
--- /dev/null
+++ b/t/template_tiny/samples/07_nesting.txt
@@ -0,0 +1,13 @@
+
+ one
+
+
+
+
+ three
+
+
+
+
+ five
+
diff --git a/t/template_tiny/samples/07_nesting.var b/t/template_tiny/samples/07_nesting.var
new file mode 100644
index 00000000..351e95ba
--- /dev/null
+++ b/t/template_tiny/samples/07_nesting.var
@@ -0,0 +1,4 @@
+$VAR1 = {
+ true => 1,
+ false => 0,
+};
diff --git a/t/template_tiny/samples/08_foreach.tt b/t/template_tiny/samples/08_foreach.tt
new file mode 100644
index 00000000..c186586e
--- /dev/null
+++ b/t/template_tiny/samples/08_foreach.tt
@@ -0,0 +1,11 @@
+People [% foo %]:
+[% FOREACH item IN list %]
+[%- item.name %] <[% item.email %]>
+[% END -%]
+Cool People:
+[% FOREACH item IN list %]
+[%- IF item.cool %]
+[%- item.name %]
+[% END %]
+[%- END -%]
+Done!
diff --git a/t/template_tiny/samples/08_foreach.txt b/t/template_tiny/samples/08_foreach.txt
new file mode 100644
index 00000000..f246254f
--- /dev/null
+++ b/t/template_tiny/samples/08_foreach.txt
@@ -0,0 +1,6 @@
+People bar:
+Adam Kennedy <adamk@cpan.org>
+Foo <No Fixed Address>
+Cool People:
+Adam Kennedy
+Done!
diff --git a/t/template_tiny/samples/08_foreach.var b/t/template_tiny/samples/08_foreach.var
new file mode 100644
index 00000000..94db524e
--- /dev/null
+++ b/t/template_tiny/samples/08_foreach.var
@@ -0,0 +1,15 @@
+$VAR1 = {
+ foo => 'bar',
+ list => [
+ {
+ name => 'Adam Kennedy',
+ email => 'adamk@cpan.org',
+ cool => 1,
+ },
+ {
+ name => 'Foo',
+ email => 'No Fixed Address',
+ cool => 0,
+ },
+ ],
+};
diff --git a/t/template_tiny/samples/09_trim.tt b/t/template_tiny/samples/09_trim.tt
new file mode 100644
index 00000000..8862e04f
--- /dev/null
+++ b/t/template_tiny/samples/09_trim.tt
@@ -0,0 +1,2 @@
+
+Hello World!
diff --git a/t/template_tiny/samples/09_trim.txt b/t/template_tiny/samples/09_trim.txt
new file mode 100644
index 00000000..c57eff55
--- /dev/null
+++ b/t/template_tiny/samples/09_trim.txt
@@ -0,0 +1 @@
+Hello World! \ No newline at end of file
diff --git a/t/template_tiny/samples/09_trim.var b/t/template_tiny/samples/09_trim.var
new file mode 100644
index 00000000..ebe3396b
--- /dev/null
+++ b/t/template_tiny/samples/09_trim.var
@@ -0,0 +1,2 @@
+$VAR1 = {};
+$VAR2 = { TRIM => 1 };
diff --git a/t/time.t b/t/time.t
new file mode 100644
index 00000000..9a6d2729
--- /dev/null
+++ b/t/time.t
@@ -0,0 +1,64 @@
+# 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;
diff --git a/t/types.t b/t/types.t
new file mode 100644
index 00000000..6b8b2db1
--- /dev/null
+++ b/t/types.t
@@ -0,0 +1,225 @@
+use 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'
+);
diff --git a/t/uri_for.t b/t/uri_for.t
new file mode 100644
index 00000000..7eff3478
--- /dev/null
+++ b/t/uri_for.t
@@ -0,0 +1,30 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'];
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+{
+ package App;
+ use Dancer2;
+ get '/foo' => sub {
+ return uri_for('/foo');
+ };
+}
+
+my $app = App->to_app;
+ok( is_coderef($app), 'Got app' );
+
+test_psgi $app, sub {
+ my $cb = shift;
+
+ is( $cb->( GET '/foo' )->code, 200, '/foo code okay' );
+ is(
+ $cb->( GET '/foo' )->content,
+ 'http://localhost/foo',
+ 'uri_for works as expected',
+ );
+};
+
+done_testing;
diff --git a/t/vars.t b/t/vars.t
new file mode 100644
index 00000000..d4e4684b
--- /dev/null
+++ b/t/vars.t
@@ -0,0 +1,35 @@
+use strict;
+use warnings;
+use Test::More import => ['!pass'];
+use Plack::Test;
+use HTTP::Request::Common;
+use Ref::Util qw<is_coderef>;
+
+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' );
+};
diff --git a/t/views/auto_page.tt b/t/views/auto_page.tt
new file mode 100644
index 00000000..7e09409d
--- /dev/null
+++ b/t/views/auto_page.tt
@@ -0,0 +1 @@
+Hey! This is Auto Page wörking.
diff --git a/t/views/beforetemplate.tt b/t/views/beforetemplate.tt
new file mode 100644
index 00000000..ca64420e
--- /dev/null
+++ b/t/views/beforetemplate.tt
@@ -0,0 +1 @@
+App is [% myname %], again, it is [% it %]
diff --git a/t/views/folder/page.tt b/t/views/folder/page.tt
new file mode 100644
index 00000000..0869e658
--- /dev/null
+++ b/t/views/folder/page.tt
@@ -0,0 +1 @@
+Page under folder.
diff --git a/t/views/index.tt b/t/views/index.tt
new file mode 100644
index 00000000..fdfd26ce
--- /dev/null
+++ b/t/views/index.tt
@@ -0,0 +1,5 @@
+[index]
+var = [% var %]
+
+before_layout_render = [% before_layout_render %]
+before_template_render = [% before_template_render %]
diff --git a/t/views/layouts/main.tt b/t/views/layouts/main.tt
new file mode 100644
index 00000000..34b5d563
--- /dev/null
+++ b/t/views/layouts/main.tt
@@ -0,0 +1,7 @@
+layout top
+var = [% var %]
+before_layout_render = [% before_layout_render %]
+---
+[% content %]
+---
+layout bottom
diff --git a/t/views/session_in_template.tt b/t/views/session_in_template.tt
new file mode 100644
index 00000000..d8edb8c1
--- /dev/null
+++ b/t/views/session_in_template.tt
@@ -0,0 +1 @@
+session.name [% session.name %]
diff --git a/t/views/template_simple_index.tt b/t/views/template_simple_index.tt
new file mode 100644
index 00000000..d104948d
--- /dev/null
+++ b/t/views/template_simple_index.tt
@@ -0,0 +1,7 @@
+this is var1="<% var1 %>" and var2=<% var2 %>
+
+another line
+
+ <% foo%> <%bar %> <%baz%>
+
+<% var1 %>/<% var1 %>
diff --git a/t/views/tokens.tt b/t/views/tokens.tt
new file mode 100644
index 00000000..94e0f415
--- /dev/null
+++ b/t/views/tokens.tt
@@ -0,0 +1,6 @@
+perl_version: [% perl_version %]
+dancer_version: [% dancer_version %]
+settings.foo: [% settings.foo %]
+params.foo: [% params.foo %]
+session.foo [% session.foo %]
+vars.foo: [% vars.foo %]
diff --git a/xt/perlcritic.rc b/xt/perlcritic.rc
new file mode 100644
index 00000000..a1ecaeb9
--- /dev/null
+++ b/xt/perlcritic.rc
@@ -0,0 +1,60 @@
+# nice output, to easily see the POD of the policy
+verbose = [%p] %m at %f line %l, near '%r'\n
+# severity of 3 is a good start (1 is very strict, 5 very tolerant)
+severity = 3
+# we want to use // without //ms
+[-RegularExpressions::RequireDotMatchAnything]
+[-RegularExpressions::RequireLineBoundaryMatching]
+[-RegularExpressions::RequireExtendedFormatting]
+minimum_regex_length_to_complain_about = 5
+[-RegularExpressions::ProhibitComplexRegexes]
+# we don't want these POD rules
+[-Documentation::RequirePodSections]
+# We don't care about POD links
+[-Documentation::RequirePodLinksIncludeText]
+# we use $@ and $!
+[-Variables::ProhibitPunctuationVars]
+# We want to be able to use Carp::Verbose in our tests scripts, so
+# we add Carp to the whitelist
+[Variables::ProhibitPackageVars]
+packages = Data::Dumper File::Find FindBin Log::Log4perl Carp
+[-ValuesAndExpressions::ProhibitEmptyQuotes]
+# I really don't think q{/} is more readable than '/'...
+[-ValuesAndExpressions::ProhibitNoisyQuotes]
+# Perl::Critic recommends Readonly, but this IS BAD!
+# we use Const::Fast instead, but this policy keeps poping up.
+[-ValuesAndExpressions::ProhibitMagicNumbers]
+# we want to be able to build DSLs
+[-Modules::ProhibitAutomaticExportation]
+# We only want the main module to provide $VERSION
+[-Modules::RequireVersionVar]
+# we want to be able to define short getters
+[-Subroutines::RequireFinalReturn]
+# we can't do @_ mesures with that one
+[-Subroutines::RequireArgUnpacking]
+# name is a common used name for methods
+# but forbidden by this policy ...
+[-Subroutines::ProhibitBuiltinHomonyms]
+# some old libs use many args, we don't want to block that for now
+[-Subroutines::ProhibitManyArgs]
+# we allo protected subs
+[-Subroutines::ProhibitUnusedPrivateSubroutines]
+# We're not under CVS! :)
+[-Miscellanea::RequireRcsKeywords]
+[TestingAndDebugging::ProhibitNoStrict]
+allow = refs
+[TestingAndDebugging::ProhibitNoWarnings]
+allow = redefine prototype
+[TestingAndDebugging::RequireUseStrict]
+equivalent_modules = strictures Moo Moo::Role
+[TestingAndDebugging::RequireUseWarnings]
+equivalent_modules = strictures Moo Moo::Role
+# we use postifx controls
+[-ControlStructures::ProhibitPostfixControls]
+[-ControlStructures::ProhibitCascadingIfElse]
+# We want to use croak everywhere instead of die
+[ErrorHandling::RequireCarping]
+# allow backtick if capture result
+[InputOutput::ProhibitBacktickOperators]
+only_in_void_context = 1
+[-Variables::ProhibitAugmentedAssignmentInDeclaration]
diff --git a/xt/perltidy.rc b/xt/perltidy.rc
new file mode 100644
index 00000000..03dce5e6
--- /dev/null
+++ b/xt/perltidy.rc
@@ -0,0 +1,33 @@
+-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
diff --git a/xt/whitespace.t b/xt/whitespace.t
new file mode 100644
index 00000000..52da40e2
--- /dev/null
+++ b/xt/whitespace.t
@@ -0,0 +1,16 @@
+use Test::Whitespaces {
+
+ dirs => [qw(
+ lib
+ script
+ t
+ tools
+ xt
+ )],
+
+ ignore => [
+ qr{t/sessions/},
+ qr{t/template_tiny/samples},
+ ],
+
+};