├── .ci.gemfile ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG ├── CONTRIBUTING ├── MIT-LICENSE ├── README.rdoc ├── Rakefile ├── doc ├── CHANGELOG.old ├── conventions.rdoc └── release_notes │ ├── 1.0.0.txt │ ├── 1.1.0.txt │ ├── 1.2.0.txt │ ├── 1.3.0.txt │ ├── 2.0.0.txt │ ├── 2.1.0.txt │ ├── 2.10.0.txt │ ├── 2.11.0.txt │ ├── 2.12.0.txt │ ├── 2.13.0.txt │ ├── 2.14.0.txt │ ├── 2.15.0.txt │ ├── 2.16.0.txt │ ├── 2.17.0.txt │ ├── 2.18.0.txt │ ├── 2.19.0.txt │ ├── 2.2.0.txt │ ├── 2.20.0.txt │ ├── 2.21.0.txt │ ├── 2.22.0.txt │ ├── 2.23.0.txt │ ├── 2.24.0.txt │ ├── 2.25.0.txt │ ├── 2.26.0.txt │ ├── 2.27.0.txt │ ├── 2.28.0.txt │ ├── 2.29.0.txt │ ├── 2.3.0.txt │ ├── 2.4.0.txt │ ├── 2.5.0.txt │ ├── 2.5.1.txt │ ├── 2.6.0.txt │ ├── 2.7.0.txt │ ├── 2.8.0.txt │ ├── 2.9.0.txt │ ├── 3.0.0.txt │ ├── 3.1.0.txt │ ├── 3.10.0.txt │ ├── 3.11.0.txt │ ├── 3.12.0.txt │ ├── 3.13.0.txt │ ├── 3.14.0.txt │ ├── 3.14.1.txt │ ├── 3.15.0.txt │ ├── 3.16.0.txt │ ├── 3.17.0.txt │ ├── 3.18.0.txt │ ├── 3.19.0.txt │ ├── 3.2.0.txt │ ├── 3.20.0.txt │ ├── 3.21.0.txt │ ├── 3.22.0.txt │ ├── 3.23.0.txt │ ├── 3.24.0.txt │ ├── 3.25.0.txt │ ├── 3.26.0.txt │ ├── 3.27.0.txt │ ├── 3.28.0.txt │ ├── 3.29.0.txt │ ├── 3.3.0.txt │ ├── 3.30.0.txt │ ├── 3.31.0.txt │ ├── 3.32.0.txt │ ├── 3.33.0.txt │ ├── 3.34.0.txt │ ├── 3.35.0.txt │ ├── 3.36.0.txt │ ├── 3.37.0.txt │ ├── 3.38.0.txt │ ├── 3.39.0.txt │ ├── 3.4.0.txt │ ├── 3.40.0.txt │ ├── 3.41.0.txt │ ├── 3.42.0.txt │ ├── 3.43.0.txt │ ├── 3.44.0.txt │ ├── 3.45.0.txt │ ├── 3.46.0.txt │ ├── 3.47.0.txt │ ├── 3.48.0.txt │ ├── 3.49.0.txt │ ├── 3.5.0.txt │ ├── 3.50.0.txt │ ├── 3.51.0.txt │ ├── 3.52.0.txt │ ├── 3.53.0.txt │ ├── 3.54.0.txt │ ├── 3.55.0.txt │ ├── 3.56.0.txt │ ├── 3.57.0.txt │ ├── 3.58.0.txt │ ├── 3.59.0.txt │ ├── 3.6.0.txt │ ├── 3.60.0.txt │ ├── 3.61.0.txt │ ├── 3.62.0.txt │ ├── 3.63.0.txt │ ├── 3.64.0.txt │ ├── 3.65.0.txt │ ├── 3.66.0.txt │ ├── 3.67.0.txt │ ├── 3.68.0.txt │ ├── 3.69.0.txt │ ├── 3.7.0.txt │ ├── 3.70.0.txt │ ├── 3.71.0.txt │ ├── 3.72.0.txt │ ├── 3.73.0.txt │ ├── 3.74.0.txt │ ├── 3.75.0.txt │ ├── 3.76.0.txt │ ├── 3.77.0.txt │ ├── 3.78.0.txt │ ├── 3.79.0.txt │ ├── 3.8.0.txt │ ├── 3.80.0.txt │ ├── 3.81.0.txt │ ├── 3.82.0.txt │ ├── 3.83.0.txt │ ├── 3.84.0.txt │ ├── 3.85.0.txt │ ├── 3.86.0.txt │ ├── 3.87.0.txt │ ├── 3.88.0.txt │ ├── 3.89.0.txt │ ├── 3.9.0.txt │ ├── 3.90.0.txt │ ├── 3.91.0.txt │ └── 3.92.0.txt ├── lib ├── roda.rb └── roda │ ├── cache.rb │ ├── plugins.rb │ ├── plugins │ ├── Integer_matcher_max.rb │ ├── _after_hook.rb │ ├── _base64.rb │ ├── _before_hook.rb │ ├── _optimized_matching.rb │ ├── _symbol_class_matchers.rb │ ├── _symbol_regexp_matchers.rb │ ├── additional_render_engines.rb │ ├── additional_view_directories.rb │ ├── all_verbs.rb │ ├── assets.rb │ ├── assets_preloading.rb │ ├── assume_ssl.rb │ ├── autoload_hash_branches.rb │ ├── autoload_named_routes.rb │ ├── backtracking_array.rb │ ├── branch_locals.rb │ ├── break.rb │ ├── caching.rb │ ├── capture_erb.rb │ ├── chunked.rb │ ├── class_level_routing.rb │ ├── class_matchers.rb │ ├── common_logger.rb │ ├── conditional_sessions.rb │ ├── content_for.rb │ ├── content_security_policy.rb │ ├── cookie_flags.rb │ ├── cookies.rb │ ├── csrf.rb │ ├── custom_block_results.rb │ ├── custom_matchers.rb │ ├── default_headers.rb │ ├── default_status.rb │ ├── delay_build.rb │ ├── delegate.rb │ ├── delete_empty_headers.rb │ ├── direct_call.rb │ ├── disallow_file_uploads.rb │ ├── drop_body.rb │ ├── each_part.rb │ ├── early_hints.rb │ ├── empty_root.rb │ ├── environments.rb │ ├── erb_h.rb │ ├── error_email.rb │ ├── error_handler.rb │ ├── error_mail.rb │ ├── exception_page.rb │ ├── filter_common_logger.rb │ ├── flash.rb │ ├── h.rb │ ├── halt.rb │ ├── hash_branch_view_subdir.rb │ ├── hash_branches.rb │ ├── hash_matcher.rb │ ├── hash_paths.rb │ ├── hash_routes.rb │ ├── head.rb │ ├── header_matchers.rb │ ├── heartbeat.rb │ ├── hmac_paths.rb │ ├── hooks.rb │ ├── host_authorization.rb │ ├── host_routing.rb │ ├── hsts.rb │ ├── indifferent_params.rb │ ├── inject_erb.rb │ ├── invalid_request_body.rb │ ├── json.rb │ ├── json_parser.rb │ ├── link_to.rb │ ├── mail_processor.rb │ ├── mailer.rb │ ├── match_affix.rb │ ├── match_hook.rb │ ├── match_hook_args.rb │ ├── middleware.rb │ ├── middleware_stack.rb │ ├── module_include.rb │ ├── multi_public.rb │ ├── multi_route.rb │ ├── multi_run.rb │ ├── multi_view.rb │ ├── multibyte_string_matcher.rb │ ├── named_routes.rb │ ├── named_templates.rb │ ├── not_allowed.rb │ ├── not_found.rb │ ├── optimized_segment_matchers.rb │ ├── optimized_string_matchers.rb │ ├── padrino_render.rb │ ├── param_matchers.rb │ ├── params_capturing.rb │ ├── part.rb │ ├── partials.rb │ ├── pass.rb │ ├── path.rb │ ├── path_matchers.rb │ ├── path_rewriter.rb │ ├── permissions_policy.rb │ ├── placeholder_string_matchers.rb │ ├── plain_hash_response_headers.rb │ ├── precompile_templates.rb │ ├── public.rb │ ├── r.rb │ ├── recheck_precompiled_assets.rb │ ├── redirect_http_to_https.rb │ ├── relative_path.rb │ ├── render.rb │ ├── render_coverage.rb │ ├── render_each.rb │ ├── render_locals.rb │ ├── request_aref.rb │ ├── request_headers.rb │ ├── response_request.rb │ ├── route_block_args.rb │ ├── route_csrf.rb │ ├── run_append_slash.rb │ ├── run_handler.rb │ ├── run_require_slash.rb │ ├── sessions.rb │ ├── shared_vars.rb │ ├── sinatra_helpers.rb │ ├── slash_path_empty.rb │ ├── static.rb │ ├── static_routing.rb │ ├── status_303.rb │ ├── status_handler.rb │ ├── streaming.rb │ ├── strip_path_prefix.rb │ ├── symbol_matchers.rb │ ├── symbol_status.rb │ ├── symbol_views.rb │ ├── timestamp_public.rb │ ├── type_routing.rb │ ├── typecast_params.rb │ ├── typecast_params_sized_integers.rb │ ├── unescape_path.rb │ └── view_options.rb │ ├── request.rb │ ├── response.rb │ ├── session_middleware.rb │ └── version.rb ├── roda.gemspec ├── spec ├── all.rb ├── assets │ ├── css │ │ ├── app.html │ │ ├── app.str │ │ ├── import.str │ │ ├── no_access.css │ │ └── raw.css │ └── js │ │ └── head │ │ ├── app.js │ │ ├── comment_1.js │ │ └── comment_2.js ├── autoload_hash_branches │ ├── a.rb │ ├── a │ │ ├── c.rb │ │ ├── d.rb │ │ └── e.rb │ └── b.rb ├── autoload_named_routes │ ├── a.rb │ ├── a │ │ ├── c.rb │ │ ├── d.rb │ │ └── e.rb │ └── b.rb ├── cache_spec.rb ├── composition_spec.rb ├── define_roda_method_spec.rb ├── env_spec.rb ├── freeze_spec.rb ├── integration_spec.rb ├── matchers_spec.rb ├── optimization_spec.rb ├── opts_spec.rb ├── plugin │ ├── Integer_matcher_max_spec.rb │ ├── _after_hook_spec.rb │ ├── additional_render_engines_spec.rb │ ├── additional_view_directories_spec.rb │ ├── all_verbs_spec.rb │ ├── assets_preloading_spec.rb │ ├── assets_spec.rb │ ├── assume_ssl_spec.rb │ ├── autoload_hash_branches_spec.rb │ ├── autoload_named_routes_spec.rb │ ├── backtracking_array_spec.rb │ ├── branch_locals_spec.rb │ ├── break_spec.rb │ ├── caching_spec.rb │ ├── capture_erb_spec.rb │ ├── chunked_spec.rb │ ├── class_level_routing_spec.rb │ ├── class_matchers_spec.rb │ ├── common_logger_spec.rb │ ├── conditional_sessions_spec.rb │ ├── content_for_spec.rb │ ├── content_security_policy_spec.rb │ ├── cookie_flags_spec.rb │ ├── cookies_spec.rb │ ├── csrf_spec.rb │ ├── custom_block_results_spec.rb │ ├── custom_matchers_spec.rb │ ├── default_headers_spec.rb │ ├── default_status_spec.rb │ ├── delay_build_spec.rb │ ├── delegate_spec.rb │ ├── delete_empty_headers_spec.rb │ ├── direct_call_spec.rb │ ├── disallow_file_uploads_spec.rb │ ├── drop_body_spec.rb │ ├── each_part_spec.rb │ ├── early_hints_spec.rb │ ├── empty_root_spec.rb │ ├── environments_spec.rb │ ├── erb_h_spec.rb │ ├── error_email_spec.rb │ ├── error_handler_spec.rb │ ├── error_mail_spec.rb │ ├── exception_page_spec.rb │ ├── filter_common_logger_spec.rb │ ├── flash_spec.rb │ ├── h_spec.rb │ ├── halt_spec.rb │ ├── hash_branch_view_subdir_spec.rb │ ├── hash_branches_spec.rb │ ├── hash_matcher_spec.rb │ ├── hash_paths_spec.rb │ ├── hash_routes_spec.rb │ ├── head_spec.rb │ ├── header_matchers_spec.rb │ ├── heartbeat_spec.rb │ ├── hmac_paths_spec.rb │ ├── hooks_spec.rb │ ├── host_authorization_spec.rb │ ├── host_routing_spec.rb │ ├── hsts_spec.rb │ ├── indifferent_params_spec.rb │ ├── inject_erb_spec.rb │ ├── invalid_request_body_spec.rb │ ├── json_parser_spec.rb │ ├── json_spec.rb │ ├── link_to_spec.rb │ ├── mail_processor_spec.rb │ ├── mailer_spec.rb │ ├── match_affix_spec.rb │ ├── match_hook_args_spec.rb │ ├── match_hook_spec.rb │ ├── middleware_spec.rb │ ├── middleware_stack_spec.rb │ ├── module_include_spec.rb │ ├── multi_public_spec.rb │ ├── multi_route_spec.rb │ ├── multi_run_spec.rb │ ├── multi_view_spec.rb │ ├── multibyte_string_matcher_spec.rb │ ├── named_routes_spec.rb │ ├── named_templates_spec.rb │ ├── not_allowed_spec.rb │ ├── not_found_spec.rb │ ├── optimized_segment_matchers_spec.rb │ ├── optimized_string_matchers_spec.rb │ ├── padrino_render_spec.rb │ ├── param_matchers_spec.rb │ ├── params_capturing_spec.rb │ ├── part_spec.rb │ ├── partials_spec.rb │ ├── pass_spec.rb │ ├── path_matchers_spec.rb │ ├── path_rewriter_spec.rb │ ├── path_spec.rb │ ├── permissions_policy_spec.rb │ ├── placeholder_string_matchers_spec.rb │ ├── plain_hash_response_headers_spec.rb │ ├── precompile_templates_spec.rb │ ├── public_spec.rb │ ├── r_spec.rb │ ├── recheck_precompiled_assets_spec.rb │ ├── redirect_http_to_https_spec.rb │ ├── relative_path_spec.rb │ ├── render_coverage_spec.rb │ ├── render_each_spec.rb │ ├── render_locals_spec.rb │ ├── render_spec.rb │ ├── request_aref_spec.rb │ ├── request_headers_spec.rb │ ├── response_request_spec.rb │ ├── route_block_args_spec.rb │ ├── route_csrf_spec.rb │ ├── run_append_slash_spec.rb │ ├── run_handler_spec.rb │ ├── run_require_slash_spec.rb │ ├── sessions_spec.rb │ ├── shared_vars_spec.rb │ ├── sinatra_helpers_spec.rb │ ├── slash_path_empty_spec.rb │ ├── static_routing_spec.rb │ ├── static_spec.rb │ ├── status_303_spec.rb │ ├── status_handler_spec.rb │ ├── streaming_spec.rb │ ├── strip_path_prefix_spec.rb │ ├── symbol_matchers_spec.rb │ ├── symbol_status_spec.rb │ ├── symbol_views_spec.rb │ ├── timestamp_public_spec.rb │ ├── type_routing_spec.rb │ ├── typecast_params_sized_integers_spec.rb │ ├── typecast_params_spec.rb │ ├── unescape_path_spec.rb │ └── view_options_spec.rb ├── plugin_spec.rb ├── redirect_spec.rb ├── request_spec.rb ├── response_spec.rb ├── route_spec.rb ├── session_middleware_spec.rb ├── session_spec.rb ├── spec_helper.rb ├── version_spec.rb └── views │ ├── _each.foo.str │ ├── _each.str │ ├── _test.erb │ ├── a.erb │ ├── a.rdoc │ ├── a1.str │ ├── a2.html │ ├── a3.erb │ ├── about.erb │ ├── about.str │ ├── about │ ├── _test.css.gz │ ├── _test.erb │ ├── _test.erb.gz │ ├── _test2.css │ ├── _test2.css.br │ ├── _test2.css.gz │ ├── _test2.css.zst │ ├── comp_test.erb │ ├── nested │ │ └── comp_test.erb │ ├── only.erb │ └── only_about.erb │ ├── additional │ ├── only.erb │ └── only_add.erb │ ├── b.erb │ ├── c.erb │ ├── comp_layout.erb │ ├── comp_test.erb │ ├── content-yield.erb │ ├── each.foo.str │ ├── each.str │ ├── fixed │ ├── comp_each_test.erb │ ├── comp_test.erb │ ├── layout.erb │ ├── local_test.erb │ └── opt_local_test.erb │ ├── home.erb │ ├── home.str │ ├── iv.erb │ ├── layout-alternative.erb │ ├── layout-yield.erb │ ├── layout-yield2.erb │ ├── layout.erb │ ├── layout.str │ ├── multiple-layout.erb │ └── multiple.erb └── www ├── config.ru ├── layout.erb ├── make_www.rb ├── pages ├── compare_to_sinatra.erb ├── development.erb ├── documentation.erb └── index.erb └── public ├── css └── roda.css ├── data ├── other │ ├── memory.txt │ └── rps.txt └── popular │ ├── memory.txt │ └── rps.txt ├── images ├── circuits.svg ├── other │ ├── memory.png │ └── rps.png ├── popular │ ├── memory.png │ └── rps.png ├── roda-logo.svg └── sinatra.png └── js └── roda.js /.ci.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | if RUBY_VERSION < '2' 4 | gem 'rake', '<10' 5 | elsif RUBY_VERSION < '2.3' 6 | gem 'rake', '<13' 7 | else 8 | gem 'rake' 9 | end 10 | 11 | if RUBY_VERSION < '2.0.0' 12 | gem 'mime-types', '< 3' 13 | gem "tilt", '<2.0.11' 14 | else 15 | gem "tilt" 16 | end 17 | 18 | if RUBY_VERSION >= '3.4' 19 | gem 'rdoc' 20 | end 21 | 22 | if RUBY_VERSION < '3.1.0' && RUBY_VERSION >= '3.0.0' 23 | gem 'json', '2.5.1' 24 | elsif RUBY_VERSION < '2.0.0' 25 | gem 'json', '<1.8.5' 26 | elsif RUBY_VERSION < '2.3.0' 27 | gem 'json', '<2.6' 28 | else 29 | gem 'json' 30 | end 31 | 32 | case RUBY_VERSION[0, 3] 33 | when '1.9', '2.0' 34 | gem 'rack', '<1.6' 35 | when '2.1', '2.2' 36 | gem 'rack', '<2' 37 | when '2.3' 38 | gem 'rack', '<2.1' 39 | when '2.5' 40 | gem 'rack', '<2.2' 41 | when '2.6' 42 | gem 'rack', '<3' 43 | when '2.7' 44 | gem 'rack', '<3.1' 45 | when '2.4', '3.3' 46 | # Test main branch of Rack for lowest and highest supported 47 | # Ruby version 48 | gem 'rack', :git => 'https://github.com/rack/rack' 49 | else 50 | gem 'rack' 51 | end 52 | 53 | if RUBY_VERSION < '2.4.0' 54 | # Until mintest 5.12.0 is fixed 55 | gem 'minitest', '5.11.3' 56 | else 57 | gem 'minitest' 58 | end 59 | 60 | if RUBY_VERSION >= '3.1.0' 61 | gem 'net-smtp' 62 | end 63 | 64 | gem "minitest-global_expectations" 65 | gem "minitest-hooks" 66 | gem "erubi" 67 | gem "rack_csrf" 68 | gem "mail" 69 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | tests: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ubuntu-latest] 18 | ruby: [ "2.0.0", 2.1, 2.3, 2.4, 2.5, 2.6, 2.7, "3.0", 3.1, 3.2, 3.3, 3.4, jruby-9.3, jruby-9.4, jruby-10.0, truffleruby-head ] 19 | include: 20 | - { os: ubuntu-22.04, ruby: "1.9.3" } 21 | - { os: ubuntu-22.04, ruby: jruby-9.1 } 22 | - { os: ubuntu-22.04, ruby: jruby-9.2 } 23 | runs-on: ${{ matrix.os }} 24 | name: ${{ matrix.ruby }} 25 | env: 26 | BUNDLE_GEMFILE: .ci.gemfile 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true 33 | - run: bundle exec rake spec_ci 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /roda-*.gem 2 | /rdoc/ 3 | /coverage/ 4 | /www/public/*.html 5 | /www/public/rdoc/ 6 | /spec/iv-*.erb 7 | /spec/pid-* 8 | /spec/render_coverage-* 9 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2023 Jeremy Evans 2 | Copyright (c) 2010-2014 Michel Martens, Damian Janowski and Cyril David 3 | Copyright (c) 2008-2009 Christian Neukirchen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/release_notes/2.10.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The json plugin now accepts a :content_type option, which will 4 | override the default Content-Type response header used for 5 | responses. 6 | 7 | * Stream#write has been added to the streaming plugin, allowing 8 | the following type of code to work: 9 | 10 | stream do |out| 11 | IO.copy_stream(StringIO.new(content), out) 12 | end 13 | 14 | = Other Improvements 15 | 16 | * Roda now works with ruby 2.3's --enable-frozen-string-literal, 17 | and all of the library files are set to use frozen string 18 | literals by default. 19 | 20 | Most of roda's plugin-specific dependencies were found to have 21 | issues with frozen string literals, and while pull requests have 22 | been sent to fix the issues, it's unlikely that you would 23 | currently be able to use --enable-frozen-string-literal in 24 | production. 25 | 26 | * The json plugin will no longer override a Content-Type header if 27 | one is already set. 28 | -------------------------------------------------------------------------------- /doc/release_notes/2.12.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An optimized_string_matchers plugin has been added, which contains 4 | optimized matchers for single strings. r.on_branch is an optimized 5 | version of r.on and r.is_exactly is optimized version of r.is: 6 | 7 | plugin :optimized_string_matchers 8 | 9 | route do |r| 10 | r.on_branch "x" do 11 | # matches /x and paths starting with /x/ 12 | r.is_exactly "y" do 13 | # matches /x/y 14 | end 15 | end 16 | end 17 | 18 | Both of these methods will work even if the strings have placeholders, 19 | but no captures will be yielded to the blocks. 20 | 21 | * The error_handler plugin now has access to the request's 22 | remaining_path when handling an error. You can then compare the 23 | remaining_path to path_info to see how much of request was already 24 | routed, which can be useful when reporting errors. 25 | 26 | = Other Improvements 27 | 28 | * String matching for strings without placeholders is now 60% faster 29 | as it uses optimized string operations instead of a regexp match. 30 | 31 | * Symbol matching is now 60% faster as it uses optimized string 32 | operations instead of a regexp match. 33 | 34 | = Backwards Compatibility 35 | 36 | * The match methods no longer automatically reset the remaining_path 37 | via ensure. This means that using non-local jumps out of the code 38 | such as begin/rescue and throw/catch will not reset remaining_path 39 | automatically. Users that want to reset remaining path in 40 | such cases should use their own ensure blocks. 41 | -------------------------------------------------------------------------------- /doc/release_notes/2.13.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The render plugin now supports :check_paths and :allowed_paths 4 | options. Setting :check_paths to true will turn on path checking of 5 | template files. By default, template files are required to be in 6 | the :views directory, otherwise an exception will be raised. Using 7 | the :check_paths option can prevent security issues when template 8 | names are derived from user input. The :allowed_paths option 9 | overrides which path prefixes are allowed. In Roda 3, :check_paths 10 | will default to true. 11 | -------------------------------------------------------------------------------- /doc/release_notes/2.14.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A symbol_status plugin has been added for using symbolic status names 4 | in response.status=: 5 | 6 | class App < Roda 7 | plugin :symbol_status 8 | 9 | route do |r| 10 | r.is "needs_authorization" 11 | response.status = :unauthorized 12 | end 13 | end 14 | 15 | = Other Improvements 16 | 17 | * The middleware plugin will now also run the application's middleware 18 | when the application is used as middleware. For example, if you 19 | have the following code in your config.ru file: 20 | 21 | class App < Roda 22 | plugin :csrf 23 | plugin :middleware 24 | route{} 25 | end 26 | 27 | use App 28 | 29 | previously, the csrf protection would not be enforced, as it uses a 30 | middleware instead of being part of the application. Now, csrf 31 | protection will be enforced. This change makes it so the Roda 32 | application operates the same way regardless of whether it is run 33 | as the rack application or used as rack middleware. 34 | 35 | Because of this change, if you are nesting roda applications using 36 | the middleware plugin, you may need to use the middleware plugin's 37 | :env_var option to specify the environment variable used to 38 | indicate to the Roda application that it is being run as middleware. 39 | 40 | = Backwards Compatibility 41 | 42 | * See above changes to the middleware plugin if you are using 43 | middleware inside a Roda application that uses the middleware 44 | plugin. 45 | -------------------------------------------------------------------------------- /doc/release_notes/2.19.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The indifferent_params plugin is now optimized when using Rack 2, 4 | using Rack 2's query_parser API, and it no longer needs to do a 5 | deep copy of the params. 6 | 7 | * The Content-Type and Content-Length headers are no longer added 8 | for 1xx, 204, 205, and 304 responses. 9 | 10 | * The assets_paths method in the assets plugin now works 11 | correctly when subresource integrity is enabled. 12 | 13 | * The asset paths are now escaped in tags by the assets and 14 | assets_preloading plugins. While it's unlikely a developer 15 | would use an asset path that requires escaping, that case is 16 | now handled correctly. 17 | 18 | * The h plugin no longer calls Rack::Utils.escape_html, instead 19 | implementing it's own html escaping. 20 | 21 | * The assets plugin now uses the h plugin, instead of calling 22 | Rack::Utils.escape_html. 23 | 24 | = Backwards Compatibility 25 | 26 | * The h plugin's html escaping no longer escapes "/", which is 27 | a behavior change if you are using any recent version of rack. 28 | The security arguments made to escape "/" could be applied to 29 | many other characters, so if you want to escape "/", you should 30 | probably use a separate method that escapes all \W characters. 31 | -------------------------------------------------------------------------------- /doc/release_notes/2.20.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The render plugin now supports :erubi as an :escape option value, 4 | which will change the plugin to use Erubi instead of Erubis as the 5 | template processor. Erubi is a simplified Erubis fork. 6 | -------------------------------------------------------------------------------- /doc/release_notes/2.21.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The streaming plugin now supports a handle_stream_error method to 4 | handle exceptions when using stream(:loop=>true). This method 5 | takes the exception and the stream object, and can be used to log 6 | errors, output errors into the stream, close the stream, ignore 7 | errors, or any other error handling. 8 | 9 | = Other Improvements 10 | 11 | * A couple of unused variable assignments have been removed, providing 12 | a minor speedup. 13 | 14 | * The specs no longer produce deprecation warnings when using Minitest 15 | 5.10. 16 | 17 | * Some verbose warnings have been removed from the specs. 18 | -------------------------------------------------------------------------------- /doc/release_notes/2.22.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An :unsupported_block_result => :raise option is now supported 4 | for Roda applications. This will raise a RodaError if an 5 | unsupported value is returned from a match block or the 6 | route block. This can make it easier to discover potential 7 | problems in the routing tree. This option may become the 8 | default behavior in Roda 3. 9 | 10 | * An :unsupported_matcher => :raise option is now supported for 11 | Roda applications. This will raise a RodaError if you use an 12 | unsupported value as a matcher. This can make it easier to 13 | discover potential problems in the routing tree. This option 14 | may become the default behavior in Roda 3. 15 | 16 | * A :verbatim_string_matcher option is now supported for Roda 17 | applications. This will make all string matchers only match 18 | the path verbatim, disallowing the use of a colon for 19 | placeholders in the string. It's recommended that users 20 | switch to using separate symbol arguments for placeholders. 21 | 22 | If you enable this option, you need to change code such as: 23 | 24 | r.is "foo/:bar" do |bar| 25 | end 26 | 27 | to: 28 | 29 | r.is "foo", :bar do |bar| 30 | end 31 | 32 | If you are looking to convert an existing routing tree 33 | from using placeholders in strings to separate symbol 34 | arguments, you can scan your routing tree for potential 35 | usage of placeholders in strings: 36 | 37 | grep ' r\..*['\''"].*:.*['\''"]' app.rb 38 | 39 | This option may become the default behavior in Roda 3, with 40 | a plugin added to support the current default behavior of 41 | allowing placeholders in string matchers. 42 | -------------------------------------------------------------------------------- /doc/release_notes/2.23.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An :explicit_cache option has been added to the render plugin. 4 | This is similar to the :cache=>false option, but instead of 5 | disabling caching completely, this disables caching by default but 6 | allows for explicit caching of templates by providing the :cache 7 | option to view/render. 8 | 9 | In development mode, Roda now defaults to :explicit_cache=>true 10 | instead of :cache=>false. 11 | 12 | * An :inherit_cache option has been added to the render plugin, 13 | making subclasses of that class start with a dup of the template 14 | cache, instead of starting with an empty template cache. This can 15 | result in less memory used. 16 | 17 | * Roda#error_email in the error_email plugin now accepts non-Exception 18 | arguments (such as strings). This can be useful in conditions that 19 | are errors you may want to notify about, where an exception hasn't 20 | been raised. 21 | 22 | * Roda#error_email_content has been added to the error_email plugin. 23 | This can be used to create the email message, which can be delivered 24 | via another mechanism, and may make testing easier. 25 | 26 | = Other Improvements 27 | 28 | * Roda.freeze in the static_routing plugin now returns self, fixing 29 | code such as Roda.freeze.app. 30 | -------------------------------------------------------------------------------- /doc/release_notes/2.25.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An error_mail plugin has been added for reporting exceptions raised 4 | via email. This is similar to the existing error_email plugin, but 5 | uses the mail library instead of net/smtp directly. If you are 6 | already using the mail library and the error_email plugin in your 7 | application, it's recommended to switch to the error_mail plugin. 8 | Example: 9 | 10 | plugin :error_mail, :to=>'to@example.com', :from=>'from@example.com' 11 | plugin :error_handler do |e| 12 | error_mail(e) 13 | 'Internal Server Error' 14 | end 15 | -------------------------------------------------------------------------------- /doc/release_notes/2.26.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The csrf plugin now supports a :skip_middleware option, which adds 4 | the methods without adding the middleware. This is designed for 5 | cases where you are using multiple rack apps, where the rack_csrf 6 | middleware is loaded in an earlier rack app, and you want to avoid 7 | the duplicate CSRF checks. 8 | 9 | = Other Improvements 10 | 11 | * The type_routing plugin now supports using multiple extensions 12 | where one extension is a suffix of another extension, such as 13 | using gz and tar.gz. 14 | -------------------------------------------------------------------------------- /doc/release_notes/2.28.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A status_303 plugin has been added, which changes the default 4 | redirect status from 302 to 303 if the HTTP version is 1.1 and 5 | the request is not a GET request. 6 | 7 | = Other Improvements 8 | 9 | * Roda is now optimized for ruby 2.3+ using frozen string literals 10 | instead of constant references. This improves performance on ruby 11 | 2.3+, and decreases performance on ruby <2.3. 12 | 13 | = Backwards Compatibility 14 | 15 | * Many now unused internal constants are now deprecated, and 16 | attempting to access them will result in deprecation warnings 17 | on ruby 2.3+. 18 | -------------------------------------------------------------------------------- /doc/release_notes/2.5.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The assets plugin now supports a :compiled_asset_host option, which 4 | specifies a hostname used to serve compiled assets. 5 | 6 | * The render plugin now supports a :cache_class option, which 7 | specificies a class to use for the thread-safe template cache. 8 | This can be used to setup LRU caching or caching that checks 9 | modify times on the underlying template files. 10 | 11 | * r.multi_run in the multi_run plugin now accepts a block, and calls 12 | the block before dispatching to the related rack application. This 13 | can be used to modify the environment before dispatching. Example: 14 | 15 | r.multi_run do |prefix| 16 | env['authenticated'] = true 17 | end 18 | 19 | = Backwards Compatibility 20 | 21 | * The :by_name option to the path plugin now defaults to true in 22 | development mode. This should only negatively affect applications 23 | that register anonymous classes with the path plugin. 24 | -------------------------------------------------------------------------------- /doc/release_notes/2.5.1.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The multi_route plugin now works correctly if the middleware plugin 4 | is loaded after it. 5 | -------------------------------------------------------------------------------- /doc/release_notes/2.6.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * :params and :params! matchers have been added to the param_matchers 4 | plugin, allowing you to match multiple params at the same time: 5 | 6 | r.on :params=>%w'foo bar' do |foo, bar| end 7 | # instead of 8 | r.on({:param=>'foo'}, :param=>'bar') do |foo, bar| end 9 | 10 | = Other Improvements 11 | 12 | * When loading the csrf plugin multiple times, instead of loading the 13 | middleware multiple times with different settings, merge options 14 | in later plugin calls into a single middleware option hash, and 15 | only load the middleware once. 16 | 17 | This allows plugins to depend on the csrf plugin, while also 18 | allowing the application to use the csrf plugin with options. 19 | 20 | * request.halt now works correctly when used inside a before hook when 21 | using the hooks plugin. 22 | -------------------------------------------------------------------------------- /doc/release_notes/2.8.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A multi_view plugin has been added, for easily setting up routing 4 | for rendering multiple views: 5 | 6 | plugin :multi_view 7 | 8 | route do |r| 9 | r.multi_view(['foo', 'bar', 'baz']) 10 | end 11 | 12 | # or: 13 | 14 | route do |r| 15 | r.multi_view(/(foo|bar|baz)/) 16 | end 17 | 18 | # or: 19 | 20 | regexp = multi_view_compile(['foo', 'bar', 'baz']) 21 | route do |r| 22 | r.multi_view(regexp) 23 | end 24 | 25 | # all are equivalent to: 26 | 27 | route do |r| 28 | r.get 'foo' do 29 | view('foo') 30 | end 31 | 32 | r.get 'bar' do 33 | view('bar') 34 | end 35 | 36 | r.get 'baz' do 37 | view('baz') 38 | end 39 | end 40 | 41 | = Other Improvements 42 | 43 | * The content_for plugin now supports haml templates. Previous only 44 | erb templates were supported. 45 | -------------------------------------------------------------------------------- /doc/release_notes/2.9.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The content_for plugin now supports passing the content as a 4 | string argument instead of a block: 5 | 6 | <% content_for :foo, "Some content" %> 7 | -------------------------------------------------------------------------------- /doc/release_notes/3.1.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A :timestamp_paths option has been added to the assets plugin to 4 | include timestamps in paths in non-compiled mode. This can fix 5 | asset staleness issues when using a caching proxy. This is 6 | not needed in compiled mode, as the asset file names include the 7 | hash of the asset. It is not the default in non-compiled mode, 8 | as few people would use a caching proxy in non-compiled mode. 9 | 10 | = Other Improvements 11 | 12 | * Make set_layout_locals and set_view_locals in branch_locals 13 | plugin work when the other is not called. 14 | 15 | * When testing support for uglifier usability as a JS asset 16 | compressor, handle case where uglifier is installed but there is 17 | no available javascript runtime. 18 | 19 | = Backwards Compatibility 20 | 21 | * The deprecated Roda.thread_safe_cache method has been removed. 22 | 23 | * The deprecated private RodaRequest#placeholder_string_matcher? 24 | method has been removed. 25 | -------------------------------------------------------------------------------- /doc/release_notes/3.12.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A common_logger plugin has been added for common log support. This 4 | offers about 30% better performance than Rack::CommonLogger, with 5 | the following differences: 6 | 7 | * When timing requests, doesn't consider middleware or proxy the 8 | body, so timing information is just the time that Roda takes 9 | to process the request. 10 | * Only looks for "Content-Length" as a header, not different 11 | capitalizations (Roda only uses "Content-Length" internally). 12 | * Logs to $stderr instead of rack.errors in request environment 13 | if a logger object is not explicitly passed. 14 | 15 | = Other Improvements 16 | 17 | * Internal before/after hook methods now use more descriptive names 18 | for easier debugging, with a naming format designed to not 19 | conflict with hook methods in external plugins. 20 | -------------------------------------------------------------------------------- /doc/release_notes/3.14.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The convert! and convert_each! methods in the typecast_params plugin 4 | now support a :raise option for handling missing parameters specified 5 | as arguments to the methods. 6 | 7 | If the :raise option is set to false for convert! and the parameter 8 | argument is missing, then no conversion is done and an empty hash 9 | is returned: 10 | 11 | typecast_params.convert!('missing', raise: false) do |tp| 12 | # ... 13 | end 14 | # => {} 15 | 16 | If the :raise option is set to false for convert_each! and a :keys 17 | option is given, any key not present is ignored and nil will be 18 | returned for the converted value 19 | 20 | typecast_params.convert_each!(:keys=>['present', 'missing'], raise: false) do |tp| 21 | tp.int('b') 22 | end 23 | # => [{'b'=>1}, nil] 24 | 25 | = Other Improvements 26 | 27 | * The :symbolize setting to the convert! and convert_each! methods in 28 | the typecast_params plugin is no longer persisted beyond the call 29 | to the method. This fixes unexpected behavior if you do: 30 | 31 | typecast_params.convert!(:symbolize=>true) do |tp| 32 | # ... 33 | end 34 | typecast_params.convert! do |tp| 35 | # ... 36 | end 37 | -------------------------------------------------------------------------------- /doc/release_notes/3.15.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The render plugin :escape option value can now be a string or an 4 | array of strings, and then the plugin will will only add the 5 | :escape template option for those specific template engines given. 6 | By default, the :escape plugin option adds the :escape template 7 | option for all engines, which breaks the usage with some engines 8 | (such as the rcsv engine). 9 | 10 | * The convert! and convert_each! methods in the typecast_params plugin 11 | now support a :skip_missing option to support not storing missing 12 | parameters: 13 | 14 | typecast_params.convert! do |tp| 15 | tp.int('missing') 16 | end 17 | # => {'missing'=>nil} 18 | typecast_params.convert!(skip_missing: false) do |tp| 19 | tp.int('missing') 20 | end 21 | # => {} 22 | -------------------------------------------------------------------------------- /doc/release_notes/3.2.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A timestamp_public plugin has been added for serving static files 4 | with paths that change based on the modification timestamp of the 5 | file. By using a new path, cached versions of the file will not 6 | be used, fixing staleness issues. Example: 7 | 8 | plugin :timestamp_public 9 | 10 | route do |r| 11 | # serves requests for /static/\d+/.* 12 | r.timestamp_public 13 | 14 | # /static/1234567890/path/to/file 15 | timestamp_path("path/to/file") 16 | end 17 | 18 | = Other Improvements 19 | 20 | * When using the assets plugin :timestamp_paths option, the 21 | timestamps now include microseconds, to make cache poisoning more 22 | difficult. 23 | -------------------------------------------------------------------------------- /doc/release_notes/3.20.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * For empty responses with status code 205, a Content-Length header 4 | is now added with a value of 0, for better conformance to RFC 7232. 5 | 6 | Similarly, when using the drop_body plugin, responses with status 7 | code 205 now have a Content-Length header added with a value of 0. 8 | -------------------------------------------------------------------------------- /doc/release_notes/3.21.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * View rendering speed is significantly improved in development mode 4 | by caching file-based templates until there has been a modification 5 | to the template file. 6 | -------------------------------------------------------------------------------- /doc/release_notes/3.22.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The render/view methods in the render plugin, when called with 4 | a single string/symbol argument (the most common case), are now 5 | up to 2.5x/4x faster by directly calling compiled template methods. 6 | This works by extracting the UnboundMethod objects that Tilt 7 | creates, and defining real methods for them, then calling those 8 | methods using send. This avoids most of the overhead of the render 9 | and view methods. The compiled template methods are defined inside 10 | a module included in the Roda app's class, so this support works 11 | even if the Roda app itself is frozen. 12 | 13 | Some plugins, such as render_locals, do not work with this 14 | optimization, and disable the use of it. The view_options plugin 15 | does work with this optimization if you are using set_view_subdir or 16 | append_view_subdir, but not if using set_view_options or 17 | set_layout_options. 18 | 19 | This optimization depends on Ruby 2.3+ and Tilt 1.2+, and will not 20 | be used on earlier versions, or if an API change in Tilt is 21 | detected. 22 | 23 | * Session deserialization is now slightly faster in the sessions 24 | plugin. 25 | -------------------------------------------------------------------------------- /doc/release_notes/3.23.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The render/view methods in the render plugin, when called with 4 | a single string/symbol argument (the most common case), are now 5 | up to 2x faster in cache: false mode by directly calling compiled 6 | template methods. This takes the performance increase in 3.22.0 7 | and applies it to cache: false mode in addition to cache: true 8 | mode. If the template file has changed, the compiled method is 9 | removed, and a new compiled method replaces it. 10 | 11 | * Template modification detection in the render plugin now uses a 12 | faster check for modification, which also avoids a race condition. 13 | 14 | * The type_routing plugin now handles requests with nothing but the 15 | extension in the request path. This fixes cases when you have 16 | one app partially route a request, and send the request to another 17 | app, and that app uses the type_routing plugin and has an r.is 18 | call at the root level. 19 | 20 | * The roda/session_middleware middleware now works correctly if the 21 | type_routing plugin is loaded into Roda itself (as opposed to a 22 | Roda subclass). 23 | 24 | * The exception_page plugin now always shows the line number for 25 | each line. Previously, it only showed the line number if it was 26 | showing the content of the line, which complicated debugging in 27 | cases where the content of the line was no longer retrievable 28 | due to file system permissions or restrictions (e.g. chroot). 29 | -------------------------------------------------------------------------------- /doc/release_notes/3.24.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The performance of the render_each plugin has been dramatically 4 | improved by calling compiled template methods directly. For a simple 5 | template, render_each performance with a single object can be about 6 | 2x faster, and render_each performance for 100 objects can be 3x 7 | (cache: false) to 9x (cache: true) faster. 8 | 9 | This optimization can be used if no options are provided to 10 | render_each, or if :local and/or :locals options are provided. Use 11 | of other options will disable this optimization. 12 | 13 | * The module_include plugin no longer calls Proc.new without a 14 | block, fixing a warning on Ruby 2.7. 15 | -------------------------------------------------------------------------------- /doc/release_notes/3.25.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The new tilt 2.0.10 private API is now supported when using 4 | compiled template methods, with up to a 33% performance increase. 5 | The older tilt private API (back to tilt 1.2) is still supported. 6 | 7 | * The performance of the render and view methods in the render plugin 8 | when called with only the :locals option are now about 75% faster 9 | by calling compiled template methods directly. 10 | 11 | * Keyword argument separation issues are now handled on Ruby 2.7+ 12 | when defining methods with blocks that accept keyword arguments. 13 | -------------------------------------------------------------------------------- /doc/release_notes/3.26.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * Asynchronous streaming is now supported in the streaming plugin, 4 | using the :async option. When using this option, streaming 5 | responses are temporarily buffered in a queue. By default, the 6 | queue is a sized queue with a maximum of 10 elements, but the 7 | queue can be specified manually via the :queue option, which 8 | can be used with async libraries that support non-blocking 9 | queues. This option is currently only supported on Ruby 2.3+. 10 | 11 | = Other Improvements 12 | 13 | * When combining multiple compiled assets into a single file, the 14 | files are now separated by a newline, fixing issues when a 15 | single line comment is used as the last line of a file. 16 | -------------------------------------------------------------------------------- /doc/release_notes/3.27.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A multibyte_string_matcher plugin has been added that supports 4 | multibyte characters in strings used as matchers. It uses a slower 5 | string matching implementation that supports multibyte characters. 6 | As multibyte strings in paths must be escaped, this also loads the 7 | unescape_path plugin. 8 | 9 | = Other Improvements 10 | 11 | * The json_parser plugin now returns expected results for invalid JSON 12 | if the params_capturing plugin is used. 13 | 14 | * lib/roda.rb has been split into multiple files for easier code 15 | navigation. 16 | -------------------------------------------------------------------------------- /doc/release_notes/3.28.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The sessions plugin now supports RodaRequest#session_created_at 4 | and RodaRequest#session_updated_at for the times of session 5 | creation and last update. 6 | 7 | = Other Improvements 8 | 9 | * The json_parser plugin now correctly parses the request body even 10 | if the request body has already been read. 11 | 12 | * The sessions plugin now correctly handles upgrading rack cookie 13 | sessions when using rack 2.0.8+. 14 | -------------------------------------------------------------------------------- /doc/release_notes/3.29.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The common_logger plugin now includes the SCRIPT_NAME when 4 | logging, for greater compatibility with typical web server 5 | logs. 6 | 7 | * The exception_page plugin now handles invalid POST data. 8 | Previously, invalid POST data would cause the exception page 9 | display to raise an exception. 10 | 11 | * An error is now raised if trying to load a plugin that is not a 12 | module or a recognized plugin symbol. 13 | 14 | * Specs and older release notes are no longer shipped in the 15 | gem, reducing gem size by over 35%. 16 | -------------------------------------------------------------------------------- /doc/release_notes/3.30.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A :relative_paths plugin option has been added to the assets 4 | plugin. This option makes the paths to the asset files in the 5 | link and script tags relative paths instead of absolute paths. 6 | 7 | = Other Improvements 8 | 9 | * The :header matcher in the header_matchers plugin now works 10 | correctly for the Content-Type and Content-Length headers, which 11 | are not prefixed with HTTP_ in the rack environment. 12 | 13 | * The run_append_slash and run_handler plugins now work correctly 14 | when used together. 15 | -------------------------------------------------------------------------------- /doc/release_notes/3.31.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A relative_path plugin has been added, adding a relative_path 4 | method that will take an absolute path and make it relative to the 5 | current request by prepending an appropriate prefix. This is 6 | helpful when using Roda as a static site generator to generate a 7 | site that can be hosted at any subpath or directly from the 8 | filesystem. 9 | 10 | * In the path plugin, the path method now accepts a :relative 11 | option for generating relative paths instead of absolute paths. 12 | -------------------------------------------------------------------------------- /doc/release_notes/3.32.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * render_each in the render_each plugin now automatically handles 4 | template names with subdirectories and extensions. Previously, these 5 | caused issues unless the :local option was provided. So now you 6 | can use: 7 | 8 | render_each(foos, "items/foo") 9 | 10 | instead of: 11 | 12 | render_each(foos, "items/foo", :local=>:foo) 13 | 14 | * each_partial has been added to the partials plugin. It operates 15 | similarly to render_each, but uses the convention for partial 16 | template naming. So this: 17 | 18 | each_partial(foos, "items/foo") 19 | 20 | is the same as: 21 | 22 | render_each(foos, "items/_foo", :local=>:foo) 23 | 24 | = Other Improvements 25 | 26 | * The :dependencies option in the assets plugin now works correctly 27 | with compiled templates in the render plugin in uncached mode 28 | (the default in development). Previously, modifying a dependency 29 | file would not result in recompiling the asset template when 30 | requesting the main file. 31 | 32 | * Method visibility issues in the following plugins have been fixed: 33 | 34 | * content_security_policy 35 | * default_headers 36 | * indifferent_params 37 | * placeholder_string_matchers 38 | * symbol_matchers 39 | 40 | Previously, these plugins made private methods public by mistake 41 | when overriding them. Additionally, Roda.freeze no longer changes 42 | the visibility of the set_default_headers private method. 43 | -------------------------------------------------------------------------------- /doc/release_notes/3.33.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The path plugin now supports a url method, allowing for returning 4 | the entire URL instead of just the path for class-based paths. 5 | 6 | * The public plugin now supports a :brotli option that will directly 7 | serve brotli-compressed files (with .br extension) similar to how the 8 | :gzip option directly serves gzipped files (with the .gz extension). 9 | -------------------------------------------------------------------------------- /doc/release_notes/3.34.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * Multiple unneeded conditionals have been removed. 4 | 5 | * pre_content and post_context sections in backtraces are no longer 6 | included in the exception_page plugin output if they would be 7 | empty. 8 | 9 | * The match_affix plugin can be loaded again with a single argument. 10 | It was originally designed to accept a single argument, but a bug 11 | introduced in 2.29.0 made it require two arguments. 12 | 13 | * Core Roda and all plugins that ship with Roda now have 100% branch 14 | coverage. 15 | 16 | * The sinatra_helpers plugin no longer emits statement not reached 17 | warnings in verbose mode. 18 | -------------------------------------------------------------------------------- /doc/release_notes/3.35.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An r plugin has been added. This plugin adds an r method for the 4 | request, useful for allowing the use of r.halt and r.redirect even 5 | in methods where the r local variable is not in scope. 6 | 7 | = Other Improvements 8 | 9 | * Attempting to load a plugin with an argument or block when the plugin 10 | does not accept arguments or a block now warns. This is because a 11 | future update to support a block or an optional argument could break 12 | the call. 13 | -------------------------------------------------------------------------------- /doc/release_notes/3.36.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A multi_public plugin has been added, which allows serving static 4 | files from multiple separate directories. This is especially 5 | useful when there are different access control requirements per 6 | directory. 7 | 8 | * The content_security_policy now supports a 9 | content_security_policy.report_to method to set the 10 | report-to directive. 11 | 12 | = Other Improvements 13 | 14 | * When using the type_routing plugin and performing type routing 15 | using the Accept request header, the Vary response header will be 16 | added or updated so that http caches do not cache a response for one 17 | type and serve it for a different type. 18 | -------------------------------------------------------------------------------- /doc/release_notes/3.37.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A custom_matchers plugin has been added, which allows using 4 | arbitrary objects as matchers, as long as the matcher has been 5 | registered. You can register matchers using the custom_matcher 6 | class method, which takes the class of the matcher, and a block 7 | which is yielded the matcher object. The block should return 8 | nil or false if the matcher doesn't match, and any other value 9 | if the matcher does match. Example: 10 | 11 | plugin :custom_matchers 12 | method_segment = Struct.new(:request_method, :next_segment) 13 | custom_matcher(method_segment) do |matcher| 14 | # self is the request instance ("r" yielded in the route block below) 15 | if matcher.request_method == self.request_method 16 | match(matcher.next_segment) 17 | end 18 | end 19 | 20 | get_foo = method_segment.new('GET', 'foo') 21 | post_any = method_segment.new('POST', String) 22 | route do |r| 23 | r.on('baz') do 24 | r.on(get_foo) do 25 | # GET method, /baz/foo prefix 26 | end 27 | 28 | r.is(post_any) do |seg| 29 | # for POST /baz/bar, seg is "bar" 30 | end 31 | end 32 | 33 | r.on('quux') do 34 | r.is(get_foo) do 35 | # GET method, /quux/foo route 36 | end 37 | 38 | r.on(post_any) do |seg| 39 | # for POST /quux/xyz, seg is "xyz" 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /doc/release_notes/3.38.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The error_email and error_mail plugins now rescue invalid parameter 4 | errors when preparing the email body, because you generally don't 5 | want your error handler to raise an exception. 6 | -------------------------------------------------------------------------------- /doc/release_notes/3.39.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The relative_path plugin is now faster if you are calling 4 | relative_path or relative_prefix more than once when handling a 5 | request. 6 | 7 | * The typecast_params.convert! method in the typecast_params plugin 8 | now handles explicit nil values the same as missing values. 9 | Explicit nil values do not generally occur in normal Rack parameter 10 | parsing, but they can occur when using the json_parser plugin to 11 | parse JSON requests. 12 | 13 | * Roda now avoids method redefinition warnings in verbose mode by 14 | using a self alias. As Ruby 3 is dropping uninitialized instance 15 | variable warnings, Roda will be verbose warning free if you are 16 | using Ruby 3. 17 | -------------------------------------------------------------------------------- /doc/release_notes/3.4.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A middleware_stack plugin has been added for more detailed control 4 | over middleware, allowing for the removal of middleware and the 5 | insertion of middleware before existing middleware. Example: 6 | 7 | plugin :middleware_stack 8 | 9 | # Remove csrf middleware 10 | middleware_stack.remove{|m, *args| m == Rack::Csrf} 11 | 12 | # Insert csrf middleware before logger middleware 13 | middleware_stack.before{|m, *args| m == Rack::CommonLogger}. 14 | use(Rack::Csrf, raise: true) 15 | 16 | # Insert csrf middleware after logger middleware 17 | middleware_stack.after{|m, *args| m == Rack::CommonLogger}. 18 | use(Rack::Csrf, raise: true) 19 | 20 | = Other Improvements 21 | 22 | * The head plugin now calls close on the response body if the body 23 | responds to close. Previously an existing response body was 24 | just ignored. 25 | -------------------------------------------------------------------------------- /doc/release_notes/3.40.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A precompile_views method has been added to the 4 | precompile_templates plugin. This method works with Roda's 5 | optimized compiled view methods, allowing additional memory 6 | sharing between parent and child processes. 7 | 8 | * A freeze_template_caches! method has been added to the 9 | precompile_templates plugin. This freezes the template caches, 10 | preventing the compilation of additional templates, useful for 11 | enforcing that only precompiled templates are used. Additionally, 12 | this speeds up access to the template caches. 13 | 14 | * RodaCache#freeze now returns the frozen internal hash, which can 15 | then be accessed without a mutex. Previously, freeze only froze 16 | the receiver and not the internal hash, so it didn't have the 17 | expected effect. 18 | 19 | = Other Improvements 20 | 21 | * The view method in the render plugin is now faster in most cases 22 | when a single argument is used. When freezing the application, 23 | an additional optimization is performed to increase the 24 | performance of the view method even further. 25 | -------------------------------------------------------------------------------- /doc/release_notes/3.41.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The performance of the render plugin's view method when passed the 4 | :content option and no other options or arguments has been improved 5 | by about 3x, by calling compiled template methods directly. 6 | 7 | * The compiled template method for the layout is cleared when the 8 | render plugin is loaded again, which can fix issues when it is 9 | loaded with different options that affect the layout. 10 | -------------------------------------------------------------------------------- /doc/release_notes/3.42.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A recheck_precompiled_assets plugin has been added, which allows 4 | for checking for updates to the precompiled asset metadata file, 5 | and automatically using the updated data. 6 | 7 | * The common_logger plugin now supports a :method plugin option to 8 | specify the method to call on the logger. 9 | 10 | = Other Improvements 11 | 12 | * Plugins and middleware that use keyword arguments are now supported 13 | in Ruby 3. 14 | 15 | * The compile_assets class method in the assets plugin now uses an 16 | atomic approach to writing the precompiled asset metadata file. 17 | 18 | * Minor method visibility issues have been fixed. The custom_matchers 19 | plugin no longer makes the unsupported_matcher request method 20 | public, and the render plugin no longer makes the _layout_method 21 | public when the application is frozen. 22 | -------------------------------------------------------------------------------- /doc/release_notes/3.43.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A host_authorization plugin has been added to verify the requested 4 | Host header is authorized. Using it can prevent DNS rebinding 5 | attacks in cases where the application can receive requests for 6 | arbitrary hosts. 7 | 8 | To check for authorized hosts in your routing tree, you call the 9 | check_host_authorization! method. For example, if you want to 10 | check for authorized hosts after serving requests for public 11 | files, you could do: 12 | 13 | plugin :public 14 | plugin :host_authorization, 'my-domain-name.example.com' 15 | 16 | route do |r| 17 | r.public 18 | check_host_authorized! 19 | 20 | # ... rest of routing tree 21 | end 22 | 23 | In addition to handling single domain names via a string, you can 24 | provide an array of domain names, a regexp to match again, or a 25 | proc. 26 | 27 | By default, requests using unauthorized hosts receive an empty 403 28 | response. If you would like to customize the response, you can 29 | pass a block when loading the plugin: 30 | 31 | plugin :host_authorization, 'my-domain-name.example.com' do |r| 32 | response.status = 403 33 | "Response Body Here" 34 | end 35 | -------------------------------------------------------------------------------- /doc/release_notes/3.44.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An optimized_segment_matchers plugin has been added that offers 4 | very fast matchers for arbitrary segments (the same segments 5 | that would be matched by the String class matcher). The 6 | on_segment method it offers accepts no arguments and yields 7 | the next segment if there is a segment. The is_segment method 8 | is similar, but only yields if the next segment is the final 9 | segment. 10 | 11 | = Other Improvements 12 | 13 | * The send_file and attachment methods in the sinatra_helpers plugin 14 | now support RFC 5987 UTF-8 and ISO-8859-1 encoded filenames, 15 | allowing modern browsers to save files with encoded chracters. For 16 | older browsers that do not support RFC 5987, unsupported characters 17 | in filenames are replaced with dashes. This is considered to be an 18 | improvement over the previous behavior of using Ruby's inspect 19 | output for the filename, which could contain backslashes (backslash 20 | is not an allowed chracter in Windows filenames). 21 | 22 | * The performance of the String class matcher has been slightly 23 | improved. 24 | -------------------------------------------------------------------------------- /doc/release_notes/3.45.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The typecast_params plugin checks now checks for null bytes by 4 | default before typecasting. If null bytes are present, it raises 5 | an error. Most applications do not require null bytes in 6 | parameters, and in some cases allowing them can lead to security 7 | issues, especially when parameters are passed to C extensions. 8 | In general, the benefit of forbidding null bytes in parameters is 9 | greater than the cost. 10 | 11 | If you would like to continue allowing null bytes, use the 12 | :allow_null_bytes option when loading the plugin. 13 | 14 | Note that this change does not affect uploaded files, since those 15 | are expected to contain null bytes. 16 | 17 | = Backwards Compatibility 18 | 19 | * The change to the typecast_params plugin to raise an error for 20 | null bytes can break applications that are expecting null bytes 21 | to be passed in parameters. Such applications should use the 22 | :allow_null_bytes option when loading the plugin. 23 | -------------------------------------------------------------------------------- /doc/release_notes/3.46.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The r.on, r.is, r.get and r.post methods (and other verb methods 4 | if using the all_verbs plugin) have now been optimized when using 5 | a single string or regexp matcher, or the String or Integer class 6 | matcher. Since those four matchers are the most common types of 7 | matchers passed to the methods, this can significantly improve 8 | routing performance (about 50% in the r10k benchmark). 9 | 10 | This optimization is automatically applied when freezing 11 | applications, if the related methods have not been modified by 12 | plugins. 13 | 14 | This optimization does come at the expense of a small decrease 15 | in routing performance (3-4%) for unoptimized cases, but the 16 | majority of applications will see a overall performance benefit 17 | from this change. 18 | 19 | * Other minor performance improvements have been made. 20 | -------------------------------------------------------------------------------- /doc/release_notes/3.47.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The r.on optimization added in 3.46.0 has been extended to optimize 4 | all single argument calls. This results in the following speedups 5 | based on argument type: 6 | 7 | * Hash matching: 10% 8 | * Array/Symbol/Class matching: 15% 9 | * Proc matching: 25% 10 | * true matching: 45% 11 | * false/nil matching: 65% 12 | 13 | * Other minor performance improvements have been made. 14 | -------------------------------------------------------------------------------- /doc/release_notes/3.48.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A named_routes plugin has been added, for defining named route 4 | blocks that you can dispatch to with r.route. This feature was 5 | previously available as part of the multi_route plugin, but there 6 | are cases where the r.route method and support for named routes is 7 | helpful even when the multi_route plugin is not used (such as when 8 | the hash_routes plugin is used instead of the multi_route plugin). 9 | The multi_route plugin now depends on the named_routes plugin, so 10 | this change should not cause any backwards compatibility issues. 11 | -------------------------------------------------------------------------------- /doc/release_notes/3.49.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The r.is optimization added in 3.46.0 has been extended to optimize 4 | all single argument calls. This results in the following speedups 5 | based on argument type: 6 | 7 | * Hash/Class matching: 20% 8 | * Symbol matching: 25% 9 | * Array matching: 35% 10 | * Proc matching: 50% 11 | * false/nil matching: 65% 12 | 13 | * Roda now uses defined?(yield) instead of block_given? internally 14 | for better performance on CRuby. defined?(yield) is faster as it is 15 | built into the VM, while block_given? is a regular method and has 16 | the overhead of calling a regular method. Note that defined?(yield) 17 | is not implemented correctly on JRuby before 9.0.0.0, so this 18 | release of Roda drops support for JRuby versions before 9.0.0.0. 19 | -------------------------------------------------------------------------------- /doc/release_notes/3.5.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A request_aref plugin has been added for configuring the behavior 4 | of the [] and []= request methods. These methods are deprecated 5 | in the current version of Rack, but Rack will only print a 6 | deprecation warning in verbose mode. With this plugin, you can 7 | choose to never warn, always warn, or raise an exception: 8 | 9 | # Don't emit a warning, allowing for the historical Rack 10 | # behavior 11 | plugin :request_aref, :allow 12 | 13 | # Always emit a warning when the method is called 14 | plugin :request_aref, :warn 15 | 16 | # Raise an exception if the method is called 17 | plugin :request_aref, :raise 18 | 19 | = Other Improvements 20 | 21 | * When using the content_for plugin and calling content_for with a 22 | block, convert the result of the block to a string before passing 23 | the result to Tilt. This can fix issues when the template class 24 | that Tilt uses does not handle non-String input. 25 | 26 | * When using the public plugin with the :gzip option, do not add the 27 | Content-Type or Content-Encoding headers if a 304 response is 28 | returned. 29 | 30 | * Add the spec/views/about directory to the gem, allowing the specs 31 | to run correctly using just the files in the gem. 32 | -------------------------------------------------------------------------------- /doc/release_notes/3.50.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An inject_erb plugin has been added, adding an inject_erb method 4 | that allows for injecting content directly into the template output 5 | for the template currently being rendered. This allows you to more 6 | easily wrap blocks in templates, by calling methods that accept 7 | template blocks and injecting content before and after the block. 8 | 9 | * A capture_erb plugin has been added, adding a capture_erb method 10 | for capturing a template block in an erb template and returning 11 | the content appended during the block as a string, instead of 12 | having the content of the template block be included directly into 13 | the template output. This can be combined with the inject_erb 14 | plugin to inject modified versions of captured blocks into template 15 | output. 16 | 17 | * The hash_routes plugin now allows calling hash_branch and hash_path 18 | without a block in order to remove the existing route handler. This 19 | is designed to be used with code reloading libraries, so that if a 20 | route file is deleted, the related hash branches/paths are also 21 | removed, without having to reload all route files. 22 | -------------------------------------------------------------------------------- /doc/release_notes/3.51.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The named_routes plugin now allows calling route without a block 4 | to remove the existing route handler. The multi_run plugin 5 | now allows calling run without an app to remove an existing handler. 6 | These changes are designed to better support code reloading 7 | libraries, so that if the related file is deleted, the related 8 | handlers are also removed, without having to reload the entire 9 | application. 10 | 11 | = Other Improvements 12 | 13 | * The error_handler plugin now avoids a method redefinition warning 14 | in verbose warning mode. 15 | 16 | = Other 17 | 18 | * Roda's primary discussion forum is now GitHub Discussions. The 19 | ruby-roda Google Group is still available for users who would 20 | prefer to use that instead. 21 | -------------------------------------------------------------------------------- /doc/release_notes/3.52.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The typecast_params plugin now supports a :date_parse_input_handler 4 | option that will be called with all input that will be passed to 5 | the date parsing methods. You can use this option to automatically 6 | truncate input, if that is perferable to raising an error (which is 7 | how recent versions of Ruby handle too-long input). 8 | 9 | = Other Improvements 10 | 11 | * The path helper methods added by the path plugin now support 12 | blocks that use keyword arguments on Ruby 3+. 13 | 14 | * The assets plugin now uses OpenSSL::Digest instead of Digest (if 15 | available) for calculating SRI digests. This is faster on Ruby 3+, 16 | where Digest no longer uses the faster OpenSSL::Digest automatically 17 | if available. 18 | 19 | * Roda.freeze now returns self when the multi_route plugin is used. 20 | This was broken (not returning self) starting in 3.48.0. 21 | -------------------------------------------------------------------------------- /doc/release_notes/3.53.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An additional_view_directories plugin has been added, which allows 4 | you to specify additional directories to look in for templates. 5 | If the template path does not exist when using the default view 6 | directory, then each additional view directory will be checked, 7 | returning the first path that exists: 8 | 9 | plugin :additional_view_directories, ['admin_views', 'public_views'] 10 | 11 | = Other Improvements 12 | 13 | * The indifferent_params plugin now avoids a deprecation warning when 14 | using the rack main branch, which will become Rack 3. 15 | -------------------------------------------------------------------------------- /doc/release_notes/3.55.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A :forward_response_headers option has been added to the middleware 4 | plugin, which uses the response headers added by the middleware 5 | as default response headers even if the middleware does not handle 6 | the response. Response headers set by the underlying application 7 | take precedence over response headers set by the middleware. 8 | 9 | * The render plugin view method now accepts a block and will pass the 10 | block to the underlying render method call. This is useful for 11 | rendering a template that yields inside of an existing layout. 12 | Previously, you had to nest render calls to do that. 13 | -------------------------------------------------------------------------------- /doc/release_notes/3.56.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * RodaRequest#http_version has been added for determining the HTTP 4 | version the request was submitted with. This will be a string 5 | such as "HTTP/1.0", "HTTP/1.1", "HTTP/2", etc. This will use the 6 | SERVER_PROTOCOL and HTTP_VERSION entries from the environment to 7 | determine which HTTP version is in use. 8 | 9 | * The status_handler method in the status_handler plugin now supports 10 | a :keep_headers option. The value for this option should be an 11 | array of header names to keep. All other headers are removed. The 12 | default behavior without the option is still to remove all headers. 13 | 14 | * A run_require_slash plugin has been added, which will skip 15 | dispatching to another rack application if the remaining path is not 16 | empty and does not start with a slash. 17 | 18 | = Other Improvements 19 | 20 | * The status_303 plugin will use 303 as the default redirect status 21 | for non-GET requests for HTTP/2 and higher HTTP versions. Previously, 22 | it only used 303 for HTTP/1.1. 23 | 24 | * The not_allowed plugin now overrides the r.root method to return 25 | 405 responses to non-GET requests to the root. 26 | 27 | * The not_allowed plugin no longer sets the body when returning 405 28 | responses using methods such as r.get and r.post. Previously, the 29 | body was unintentionally set to the same value as the Allow header. 30 | 31 | * When using the Rack master branch (what will become Rack 3), Roda 32 | only requires the parts of rack that it uses, instead of requiring 33 | rack and relying on autoload to load the parts of rack in use. 34 | -------------------------------------------------------------------------------- /doc/release_notes/3.57.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * hash_branches and hash_paths plugins have been split off from the 4 | hash_routes plugin, allowing you to use only those parts instead 5 | of all of hash_routes. 6 | 7 | The hash_branches plugin supports the hash_branch class method 8 | and r.hash_branches routing method. 9 | 10 | The hash_paths plugin supports the hash_path class method and 11 | r.hash_paths routing method. 12 | 13 | The hash_routes plugin functions as it did previously by 14 | requiring the hash_branches and hash_paths plugins. It adds 15 | the hash_routes DSL and r.hash_routes routing method. 16 | 17 | * A hash_branch_view_subdir has been added. It builds on the 18 | view_options plugin and new hash_branches plugin, automatically 19 | appending a view subdirectory for each successful hash branch. 20 | This can DRY up code that uses a separate view subdirectory for 21 | each branch. 22 | 23 | = Other Improvements 24 | 25 | * Unprintable characters are now hex escaped in the output of the 26 | common_logger plugin. This can protect users who use software 27 | that respects shell escape sequences to view the logs. 28 | 29 | = Backwards Compatibility 30 | 31 | * The static_routing plugin now depends on the hash_paths plugin 32 | instead of the hash_routes plugin, so you will need to update 33 | your application to explicitly load the hash_routes plugin if 34 | you were relying on static_routing to implicitly load it. 35 | -------------------------------------------------------------------------------- /doc/release_notes/3.58.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A filter_common_logger plugin has been added, allowing you to skip 4 | logging of certain requests in the common_logger plugin. This 5 | allows you to only log requests for certain paths, or only log 6 | requests for certain types of responses. 7 | 8 | = Other Improvements 9 | 10 | * The heartbeat plugin is now compatible with recent changes in the 11 | rack master branch (what will be rack 3). 12 | 13 | * The exception_page plugin will now use Exception#detailed_message 14 | on Ruby 3.2+, preserving the did_you_mean and error_highlight 15 | information. Additionally, the display of exception messages 16 | has been improved. 17 | -------------------------------------------------------------------------------- /doc/release_notes/3.59.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An additional_render_engines plugin has been added, for considering 4 | multiple render engines for templates. If the template path does not 5 | exist for the default render engine, then each additional render 6 | engine will be checked, returning the first path that exists: 7 | 8 | plugin :additional_render_engines, ['haml', 'str'] 9 | 10 | This is similar to the additional_view_directories plugin added in 11 | 3.53.0. Both plugins can be used if you want to consider multiple 12 | view directories and multiple render engines. 13 | 14 | = Other Improvements 15 | 16 | * A typo in a private method name in the delete_empty_headers plugin 17 | has been fixed. 18 | -------------------------------------------------------------------------------- /doc/release_notes/3.6.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An early_hints plugin has been added for senting 103 Early Hint 4 | responses. This is currently only supported on puma 3.11+, and 5 | can allow for improved performance by letting the requestor know 6 | which related files will be needed by the request. 7 | 8 | * An :early_hints option has been added to the assets plugin. If 9 | given, calling the assets method will also issue an early hint 10 | for the related assets. 11 | 12 | * A :wrap option has been added to the json_parser plugin. If set 13 | to :always, all uploaded json data will be stored using a hash 14 | with a "_json" key. If set to :unless_hash, uploaded json data 15 | will only be wrapped in such a matter if it is not already a hash. 16 | 17 | Using the :wrap option can fix problems when using r.params when 18 | the uploaded JSON data is an array and not a hash. However, it 19 | does change the behavior of r.POST. It is possible to handle 20 | uploaded JSON array data without the :wrap option by using r.GET 21 | and r.POST directly instead of using r.params. 22 | -------------------------------------------------------------------------------- /doc/release_notes/3.61.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The typecast_params plugin now limits input bytesize for integer, 4 | float, and date/time typecasts. If the input is over the allowed 5 | bytesize, typecasting will fail. This prevents issues with trying 6 | to typecast arbitrarily large input. 7 | 8 | * The default Integer class matcher now limits integer segments to 9 | 100 characters by default, also to prevent issues with typecasting 10 | arbitrarily large input. Segments larger than 100 characters will 11 | no longer be matched by the Integer class matcher. 12 | 13 | = Backwards Compatibility 14 | 15 | * If the input bytesize limits in the typecast_params plugin cause 16 | issues in your application, you can use the :skip_bytesize_checking 17 | option when loading the plugin to disable the checks. 18 | 19 | * If the default Integer class matcher limit causes problems in your 20 | application, you can use the class_matchers plugin to override the 21 | matcher to not use a limit: 22 | 23 | plugin :class_matchers 24 | class_matcher(Integer, /(\d+)/){|a| [a.to_i]} 25 | -------------------------------------------------------------------------------- /doc/release_notes/3.63.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An autoload_hash_branches plugin has been added for autoloading 4 | route files for each hash branch, instead of requiring the route 5 | files be loaded up front. For example, to automatically load a 6 | route file for a hash branch on the first request to that branch: 7 | 8 | plugin :autoload_hash_branches 9 | autoload_hash_branch('branch_name', '/path/to/file') 10 | autoload_hash_branch('namespace', 'branch_name', '/path/to/file') 11 | 12 | The route file loaded should define the expected hash branch. 13 | 14 | It is common to have route files stored in a directory, with the 15 | file name matching the branch name. In that case, you can set 16 | autoloading for all route files in a given directory: 17 | 18 | plugin :autoload_hash_branches 19 | autoload_hash_branch_dir('/path/to/dir') 20 | autoload_hash_branch_dir('namespace', '/path/to/dir') 21 | 22 | Note that autoloading hash branches does not work if the application 23 | is frozen. This plugin should only be used in development mode for 24 | faster startup, or when running tests on a subset of the application 25 | in order to avoid loading parts of the application unrelated to what 26 | is being tested. 27 | 28 | * The mailer plugin now supports a :terminal plugin option to make 29 | the r.mail method force a terminal match, similar to how r.get 30 | and other HTTP verb methods work in standard Roda. This behavior 31 | will become the default in Roda 4. 32 | 33 | = Other Improvements 34 | 35 | * The mailer plugin now correctly sets the content_type of the body 36 | for emails with attachments when using mail 2.8.0+. 37 | -------------------------------------------------------------------------------- /doc/release_notes/3.64.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An erb_h plugin has been added for faster HTML escaping using 4 | erb/escape. erb 4 added erb/escape and it is included in Ruby 3.2. 5 | 6 | The erb_h plugin is added as a separate plugin because it changes 7 | the behavior of the h method. The h method added by the h plugin 8 | will always return a new string, but the h method added by the 9 | erb_h plugin will return the argument if the argument is a 10 | string that does not need escaping. By avoiding unnecessary 11 | string allocations, use of the erb_h plugin can speed up HTML 12 | escaping. 13 | 14 | = Other Improvements 15 | 16 | * The autoload_hash_branches plugin added in Roda 3.63.0 will now 17 | eagerly load the hash branches when freezing the application, 18 | allowing the application to continue to work after being frozen. 19 | Additionally, file paths for the hash branches will now be 20 | automatically expanded, allowing the use of relative file paths. 21 | 22 | = Backwards Compatibility 23 | 24 | * The expanding of file paths in the autoload_hash_branches plugin 25 | can break applications that were providing relative paths and 26 | expecting them to be looked up using the Ruby load path. 27 | -------------------------------------------------------------------------------- /doc/release_notes/3.65.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An autoload_named_routes plugin has been added for autoloading files 4 | for a named route setup by the named_routes plugin when there is a 5 | request for that route. 6 | 7 | = Other Improvements 8 | 9 | * The path method in the path plugin now supports a :class_name option. 10 | You can set this option to true and use a class name String/Symbol 11 | to register paths for classes without referencing the related class, 12 | useful when autoloading the class. 13 | -------------------------------------------------------------------------------- /doc/release_notes/3.66.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A render_coverage plugin has been added, which will cause compiled 4 | template code to be saved to a folder and loaded using load instead 5 | of eval. This allows for coverage to work for the compiled template 6 | code in Ruby versions before 3.2. It can also allow for verbose 7 | syntax warnings in compiled template code (ignored by eval), and 8 | can also be useful for static analysis of compiled template code. 9 | This plugin requires tilt 2.1+. 10 | 11 | * The exception_page plugin now supports exception_page_{css,js} 12 | instance methods for overriding the CSS and JavaScript on the 13 | generated exception page. 14 | 15 | = Other Improvements 16 | 17 | * Using inline templates (render/view :inline option) no longer keeps 18 | a reference to the Roda instance that caches the template. 19 | 20 | = Backwards Compatibility 21 | 22 | * The Render::TemplateMtimeWrapper API has changed. Any external 23 | use of this class needs to be updated. 24 | -------------------------------------------------------------------------------- /doc/release_notes/3.67.0.txt: -------------------------------------------------------------------------------- 1 | = New Feature 2 | 3 | * A custom_block_results plugin has been added for custom handling 4 | of block results. This allows routing blocks to return 5 | arbitrary objects instead of just String, nil, and false, and 6 | to have custom handling for them. For example, if you want to 7 | be able to have your routing blocks return the status code to use, 8 | you could do: 9 | 10 | plugin :custom_block_results 11 | 12 | handle_block_result Integer do |result| 13 | response.status_code = result 14 | end 15 | 16 | route do |r| 17 | 200 18 | end 19 | 20 | While the expected use of the handle_block_result method is with 21 | class arguments, you can use any argument that implements an 22 | appropriate === method. 23 | 24 | The symbol_views and json plugins, which support additional block 25 | results, now use the custom_block_results plugin internally. 26 | -------------------------------------------------------------------------------- /doc/release_notes/3.68.0.txt: -------------------------------------------------------------------------------- 1 | = New Feature 2 | 3 | * Roda.run in the multi_run plugin now accepts blocks, to allow 4 | autoloading of apps to dispatch to: 5 | 6 | class App < Roda 7 | plugin :multi_run 8 | 9 | run("other_app"){OtherApp} 10 | 11 | route do |r| 12 | r.multi_run 13 | end 14 | end 15 | 16 | With the above example, the block is not evaluated until a 17 | request for the /other_app branch is received. If OtherApp is 18 | autoloaded, this can speed up application startup and partial 19 | testing. When freezing the application (for production use), 20 | the block is eagerly loaded, so that requests to the 21 | /other_app branch do not call the block on every request. 22 | -------------------------------------------------------------------------------- /doc/release_notes/3.69.0.txt: -------------------------------------------------------------------------------- 1 | = New Feature 2 | 3 | * The symbol_matcher method in the symbol_matchers plugin now 4 | supports a block to allow for type conversion of matched 5 | segments: 6 | 7 | symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| 8 | [Date.new(y.to_i, m.to_i, d.to_i)] 9 | end 10 | 11 | route do |r| 12 | r.on :date do |date| 13 | # date is an instance of Date 14 | end 15 | end 16 | 17 | As shown above, the block should return an array of objects to yield 18 | to the match block. 19 | 20 | If you have a segment match the passed regexp, but decide during block 21 | processing that you do not want to treat it as a match, you can have the 22 | block return nil or false. This is useful if you want to make sure you 23 | are using valid data: 24 | 25 | symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| 26 | y = y.to_i 27 | m = m.to_i 28 | d = d.to_i 29 | [Date.new(y, m, d)] if Date.valid_date?(y, m, d) 30 | end 31 | 32 | When providing a block when using the symbol_matchers method, that 33 | symbol may not work with the params_capturing plugin. 34 | -------------------------------------------------------------------------------- /doc/release_notes/3.70.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A plain_hash_response_headers plugin has been added. On Rack 3, 4 | this changes Roda to use a plain hash for response headers (as it 5 | does on Rack 2), instead of using Rack::Headers (the default on 6 | Rack 3). For a minimal app, using this plugin can almost double 7 | the performance on Rack 3. Before using this plugin, you should 8 | make sure that all response headers set explictly in your 9 | application are already lower-case. 10 | 11 | = Improvements 12 | 13 | * Roda now natively uses lower-case for all response headers set 14 | implicitly when using Rack 3. Previously, Roda used mixed-case 15 | response headers and had Rack::Headers handle the conversion to 16 | lower-case (Rack 3 requires lower-case response headers). Note 17 | that Rack::Headers is still used for response headers by default 18 | on Rack 3, as applications may not have converted to using 19 | lower-case response headers. 20 | -------------------------------------------------------------------------------- /doc/release_notes/3.71.0.txt: -------------------------------------------------------------------------------- 1 | = New Feature 2 | 3 | * A match_hook_args plugin has been added. This is similar to the 4 | existing match_hook plugin, but passes through the matchers and 5 | block arguments (values yielded to the match block). Example: 6 | 7 | plugin :match_hook_args 8 | 9 | add_match_hook do |matchers, block_args| 10 | logger.debug("matchers: #{matchers.inspect}. #{block_args.inspect} yielded.") 11 | end 12 | 13 | # Term is an implicit matcher used for terminating matches, and 14 | # will be included in the array of matchers yielded to the match hook 15 | # if a terminating match is used. 16 | term = self.class::RodaRequest::TERM 17 | 18 | route do |r| 19 | r.root do 20 | # for a request for / 21 | # matchers: nil, block_args: nil 22 | end 23 | 24 | r.on 'a', ['b', 'c'], Integer do |segment, id| 25 | # for a request for /a/b/1 26 | # matchers: ["a", ["b", "c"], Integer], block_args: ["b", 1] 27 | end 28 | 29 | r.get 'd' do 30 | # for a request for /d 31 | # matchers: ["d", term], block_args: [] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /doc/release_notes/3.73.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The middleware plugin now accepts a :next_if_not_found option. 4 | This allows the middleware plugin to pass the request to the next 5 | application if the current application handles the request but 6 | ends up calling the not_found handler. With the following 7 | middleware: 8 | 9 | class Mid < Roda 10 | plugin :middleware 11 | 12 | route do |r| 13 | r.on "foo" do 14 | r.get "bar" do 15 | 'bar' 16 | end 17 | end 18 | end 19 | end 20 | 21 | Requests for /x would be forwarded to the next application, since 22 | the application doesn't handle the request, but requests for /foo/x 23 | would not be, because the middleware is partially handling the 24 | request in the r.on "foo" block. With the :next_if_not_found 25 | option, only requests for /foo/bar would be handled by the 26 | middleware, and all other requests would be forwarded to the next 27 | application. 28 | 29 | = Other Improvements 30 | 31 | * The sessions and route_csrf plugins no longer depend on the base64 32 | library. base64 will be removed from Ruby's standard library 33 | starting in Ruby 3.4. 34 | -------------------------------------------------------------------------------- /doc/release_notes/3.74.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A redirect_http_to_https plugin has been added, redirecting HTTP 4 | requests to the same path on an HTTPS site. Using the routing tree, 5 | you can control where to do the redirection, which allows you to 6 | easily have part of your site accessible via HTTP, with sensitive 7 | sections requiring HTTPS: 8 | 9 | plugin :redirect_http_to_https 10 | 11 | route do |r| 12 | # routes available via both HTTP and HTTPS 13 | r.redirect_http_to_https 14 | # routes available only via HTTPS 15 | end 16 | 17 | If you want to redirect to HTTPS for all routes in the routing tree, you 18 | can have r.redirect_http_to_https as the very first method call in the 19 | routing tree. Note that in Roda it is possible to handle routing before 20 | the normal routing tree using before hooks. The static_routing and 21 | heartbeat plugins use this feature. If you would like to handle routes 22 | before the normal routing tree, you can setup a before hook: 23 | 24 | plugin :hooks 25 | 26 | before do 27 | request.redirect_http_to_https 28 | end 29 | -------------------------------------------------------------------------------- /doc/release_notes/3.75.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A cookie_flags plugin has been added, for overriding, warning, or 4 | raising for incorrect cookie flags. The plugin by default checks 5 | whether the secure, httponly, and samesite=strict flags are set. 6 | The default behavior is to add the appropriate flags if they are 7 | not set, and change the samesite flag to strict if it is set to 8 | something else. You can configure the flag checking behavior 9 | via the :httponly, :same_site, and :secure options. 10 | 11 | You can configure the action the plugin takes via the :action option. 12 | The default action is to modify the flags, but the :action option can 13 | be set to :raise, :warn, or :warn_and_modify to override the behavior. 14 | 15 | The recommended way to use the plugin is to use it during testing, 16 | and specify action: :raise, so you can catch places where cookies 17 | are set with the wrong flags. Then you can fix those places to 18 | use the correct flags, which is better than relying on the plugin 19 | at runtime in production to fix incorrect flags. 20 | -------------------------------------------------------------------------------- /doc/release_notes/3.76.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A break plugin has been added, allowing you to use break from 4 | inside a routing block and continue routing after the block. This 5 | offers the same feature as the pass plugin, but using the standard 6 | break keyword instead of the r.pass method. 7 | 8 | * The error_mail and error_email features now both accept a :filter 9 | plugin option. The value should respond to call with two arguments. 10 | The first arguments is the key, and the second is the value, and 11 | should return a truthy value if the value should be filtered. This 12 | will be used for filtering parameter values, ENV values, and session 13 | values in the generated emails. 14 | 15 | = Other Improvements 16 | 17 | * On Ruby 3.3+, the middleware plugin sets a temporary class name for 18 | the created middleware, based on the class name of the Roda app. 19 | -------------------------------------------------------------------------------- /doc/release_notes/3.77.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The route_csrf plugin now supports formaction/formmethod attributes 4 | in forms. A csrf_formaction_tag method has been added for creating 5 | a hidden input for a particular path and method. When a form is 6 | submitted, the check_csrf! method will fix check for a path-specific 7 | csrf token (set by the hidden tag added by the csrf_formaction_tag 8 | method), before checking for the default csrf token. 9 | -------------------------------------------------------------------------------- /doc/release_notes/3.8.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The convert_each! method in the typecast_params plugin now 4 | accepts a Proc or Method value for the :keys option. The proc 5 | or method is called with the current array or hash that 6 | typecast params is operating on, and should return an 7 | array of keys to use for the conversion. 8 | 9 | * The convert_each! method in the typecast_params plugin will 10 | now automatically handle hashes with keys from '0'..'N', 11 | without a :keys option being provided. 12 | 13 | This makes it possible to handle parameter names such as 14 | foo[0][bar], foo[0][baz], foo[1][bar], and foo[1][baz], if you 15 | want to avoid the issues related to rack's issues when parsing 16 | array parameters. 17 | 18 | = Other Improvements 19 | 20 | * The Roda::RodaVersionNumber constant has been added for easier 21 | version comparisons. It is 30080 for version 3.8.0. 22 | 23 | = Backwards Compatibility 24 | 25 | * When an unsupported type is given as value of the :keys option 26 | to the convert_each! method in the typecast_params plugin, a 27 | ProgrammerError exception is now raised. 28 | -------------------------------------------------------------------------------- /doc/release_notes/3.80.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The hmac_paths plugin now supports a :namespace option for both hmac_path and 4 | r.hmac_path. The :namespace option makes the generated HMAC values unique 5 | per namespace, allowing easy use of per user/group HMAC paths. This can 6 | be useful if the same path will show different information to different 7 | users/groups, and you want to prevent path enumeration for each user/group 8 | (not allow paths enumerated by one user/group to be valid for a different 9 | user/group). Example: 10 | 11 | hmac_path('/widget/1', namespace: '1') 12 | # => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1" 13 | 14 | hmac_path('/widget/1', namespace: '2') 15 | # => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1" 16 | 17 | The HMAC path created with namespace: '1' will only be valid when calling 18 | r.hmac_path with namespace: '1' (similar for namespace: '2'). 19 | 20 | It is expected that the most common use of the :namespace option is to 21 | reference session values, so the value of each path depends on the logged in 22 | user. You can use the :namespace_session_key plugin option to set the 23 | default namespace for both hmac_path and r.hmac_path: 24 | 25 | plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes', 26 | namespace_session_key: 'account_id' 27 | 28 | This will use session['account_id'] (converted to a string) as the namespace 29 | for both hmac_path and r.hmac_path, unless a specific :namespace option is 30 | given, making it simple to implement per user/group HMAC paths across an 31 | application. 32 | -------------------------------------------------------------------------------- /doc/release_notes/3.81.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The hmac_paths plugin now supports :until and :seconds options for 4 | hmac_path, to create a path that is only valid for a specific amount of 5 | time. :until sets a specific time that the path will be valid until, 6 | and :seconds makes the path only valid for the given number of seconds. 7 | 8 | hmac_path('/widget/1', until: Time.utc(2100)) 9 | # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1" 10 | 11 | Requests for the path after the given time will not be matched by 12 | r.hmac_path. 13 | 14 | = Other Improvements 15 | 16 | * The early_hints plugin now correctly follows the Rack 3 SPEC when 17 | using Rack 3. This was not caught previously because Rack only 18 | added official support for early_hints in the last month. 19 | 20 | * Ruby 3.4 backtraces are now parsed correctly in the exception_page 21 | plugin. 22 | 23 | * Some plugins that accept a block no longer issue an unused block 24 | warning on Ruby 3.4. 25 | -------------------------------------------------------------------------------- /doc/release_notes/3.83.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An assume_ssl plugin has been added. This plugin is designed for 4 | cases where the application is being fronted by an SSL-terminating 5 | reverse proxy that does not set the X-Forwarded-Proto or similar 6 | header to indicate it is forwarding an SSL request. 7 | -------------------------------------------------------------------------------- /doc/release_notes/3.84.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An hsts plugin has been added to easily add an appropriate 4 | Strict-Transport-Security header: 5 | 6 | plugin :hsts 7 | # Strict-Transport-Security: max-age=63072000; includeSubDomains 8 | 9 | plugin :hsts, preload: true 10 | # Strict-Transport-Security: max-age=63072000; includeSubDomains; preload 11 | 12 | = Other Improvements 13 | 14 | * The gem size has been reduced 25% by removing documentation. 15 | -------------------------------------------------------------------------------- /doc/release_notes/3.86.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A conditional_sessions plugin has been added. This allows you to 4 | only support sessions for a subset of the application's requests. 5 | You pass a block when loading the plugin, and sessions are only 6 | supported if the block returns truthy. The block is evaluated 7 | in request scope. 8 | 9 | As an example, if you do not want to support sessions for request 10 | paths starting with /static, you could use: 11 | 12 | plugin :conditional_sessions, secret: ENV["SECRET"] do 13 | !path_info.start_with?('/static') 14 | end 15 | 16 | With this example, if the request path starts with /static: 17 | 18 | * The request methods +session+, +session_created_at+, and 19 | +session_updated_at+ all raise an exception. 20 | * The request +persist_session+ and route scope +clear_session+ 21 | methods do nothing and return nil. 22 | 23 | Options passed when loading the plugin are passed to the sessions 24 | plugin. 25 | 26 | * In the content_security_policy plugin, you can now call 27 | response.skip_content_security_policy! to skip the setting of the 28 | response header. 29 | 30 | * In the permissions_policy plugin, you can now call 31 | response.skip_permissions_policy! to skip the setting of the 32 | response header. 33 | 34 | = Other Improvements 35 | 36 | * When using the autoload_hash_branches and/or autoload_named_routes 37 | plugins, Roda.freeze now works correctly if the Roda class is 38 | already frozen. 39 | -------------------------------------------------------------------------------- /doc/release_notes/3.89.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The render plugin now supports an :assume_fixed_locals option, 4 | which allows for better caching when all templates use fixed 5 | locals, by using a simplified cache key, and avoiding duplicate 6 | cache entries for templates rendered both with and without locals. 7 | 8 | Additionally, when this plugin option is set, calling template 9 | methods is now faster if the following are true: 10 | 11 | * The application is frozen 12 | * Template caching is enabled 13 | * Ruby version is 3+ 14 | 15 | * A part plugin has been added, which simplifies rendering a template 16 | with locals: 17 | 18 | # render plugin 19 | render(:template, locals: {foo: 'bar'}) 20 | 21 | # part plugin 22 | part(:template, foo: 'bar') 23 | 24 | In addition to offering a nicer API if you only need to provide 25 | locals, the part method can also be faster if all of the 26 | following are true: 27 | 28 | * The application is frozen 29 | * The :assume_fixed_locals render plugin option is set 30 | * Template caching is enabled 31 | * Ruby version is 3+ (even faster on Ruby 3.4+) 32 | 33 | = Other Improvements 34 | 35 | * The mailer plugin's mail and sendmail class methods now support 36 | keyword arguments and pass them as keywords to the r.mail blocks 37 | in the routing tree. 38 | -------------------------------------------------------------------------------- /doc/release_notes/3.90.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The send_file method in the sinatra_helpers plugin now returns 4 | a response body that implements to_path. 5 | 6 | * Roda now sets a temporary name for the remaining anonymous 7 | modules and classes on Ruby 3.3+. 8 | 9 | * The common_logger plugin now escapes embedded newlines. These 10 | should only be present if the server is broken and including 11 | newlines in things it shouldn't. 12 | -------------------------------------------------------------------------------- /doc/release_notes/3.91.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The render_each method in the render_each plugin now accepts a 4 | block. If passed a block, instead of returning a concatenation 5 | of the rendered template output, it yields each rendered template 6 | output, and returns nil. This allows for use in the case where you 7 | want to wrap the template output: 8 | 9 | <% render_each([1,2,3], :foo) do |text| %> 10 |

<%= text %>

11 | <% end %> 12 | 13 | If can also be used to reduce memory usage even in the case where 14 | you are not wrapping template output. Instead of: 15 | 16 | <%= render_each([1,2,3], :foo) %> 17 | 18 | You can do: 19 | 20 | <% render_each([1,2,3], :foo) %><%= body %><% end %> 21 | 22 | This will avoid building a potentially large unnecessary intermediate 23 | string. 24 | 25 | * The capture_erb plugin now supports a returns: :buffer method and 26 | plugin option. When this option is provided, the capture_erb method 27 | returns the buffer instead of the return value of the block passed 28 | to it. This better handles cases where the template ends in a 29 | conditional: 30 | 31 | <% value = capture_erb do %> 32 | Some content here. 33 | <% if something %> 34 | Some more content here. 35 | <% end %> 36 | <% end %> 37 | -------------------------------------------------------------------------------- /doc/release_notes/3.92.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An each_part plugin has been added, offering a simpler method for 4 | using render_each with locals: 5 | 6 | # With render_each: 7 | render_each(array_of_foos, :foo, locals: {bar: 1}) 8 | 9 | # With each_part: 10 | each_part(array_of_foos, :foo, bar: 1) 11 | 12 | The each_part provides similar benefits to the render_each plugin 13 | that the part plugin provides to the render plugin. The each_part 14 | method has been optimized to work with the render plugin's 15 | :assume_fixed_locals option. 16 | 17 | = Other Improvements 18 | 19 | * The render_each plugin has been optimized to work with the render 20 | plugin's :assume_fixed_locals option. 21 | -------------------------------------------------------------------------------- /lib/roda/cache.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require "thread" 4 | 5 | class Roda 6 | # A thread safe cache class, offering only #[] and #[]= methods, 7 | # each protected by a mutex. 8 | class RodaCache 9 | # Create a new thread safe cache. 10 | def initialize 11 | @mutex = Mutex.new 12 | @hash = {} 13 | end 14 | 15 | # Make getting value from underlying hash thread safe. 16 | def [](key) 17 | @mutex.synchronize{@hash[key]} 18 | end 19 | 20 | # Make setting value in underlying hash thread safe. 21 | def []=(key, value) 22 | @mutex.synchronize{@hash[key] = value} 23 | end 24 | 25 | # Return the frozen internal hash. The internal hash can then 26 | # be accessed directly since it is frozen and there are no 27 | # thread safety issues. 28 | def freeze 29 | @hash.freeze 30 | end 31 | 32 | private 33 | 34 | # Create a copy of the cache with a separate mutex. 35 | def initialize_copy(other) 36 | @mutex = Mutex.new 37 | other.instance_variable_get(:@mutex).synchronize do 38 | @hash = other.instance_variable_get(:@hash).dup 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/roda/plugins/_after_hook.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative 'error_handler' 4 | 5 | # 6 | class Roda 7 | module RodaPlugins 8 | # RODA4: Remove 9 | register_plugin(:_after_hook, ErrorHandler) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/roda/plugins/_base64.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | module Base64_ 7 | class << self 8 | if RUBY_VERSION >= '2.4' 9 | def decode64(str) 10 | str.unpack1("m0") 11 | end 12 | # :nocov: 13 | else 14 | def decode64(str) 15 | str.unpack("m0")[0] 16 | end 17 | # :nocov: 18 | end 19 | 20 | def urlsafe_encode64(bin) 21 | str = [bin].pack("m0") 22 | str.tr!("+/", "-_") 23 | str 24 | end 25 | 26 | def urlsafe_decode64(str) 27 | decode64(str.tr("-_", "+/")) 28 | end 29 | end 30 | end 31 | 32 | register_plugin(:_base64, Base64_) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/roda/plugins/_before_hook.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # Internal before hook module, not for external use. 7 | # Allows for plugins to configure the order in which 8 | # before processing is done by using _roda_before_* 9 | # private instance methods that are called in sorted order. 10 | # Loaded automatically by the base library if any _roda_before_* 11 | # methods are defined. 12 | module BeforeHook # :nodoc: 13 | module InstanceMethods 14 | # Run internal before hooks - Old Dispatch API. 15 | def call(&block) 16 | # RODA4: Remove 17 | super do 18 | _roda_before 19 | instance_exec(@_request, &block) # call Fallback 20 | end 21 | end 22 | 23 | # Run internal before hooks before running the main 24 | # roda route. 25 | def _roda_run_main_route(r) 26 | _roda_before 27 | super 28 | end 29 | 30 | private 31 | 32 | # Default empty implementation of _roda_before, usually 33 | # overridden by Roda.def_roda_before. 34 | def _roda_before 35 | end 36 | end 37 | end 38 | 39 | register_plugin(:_before_hook, BeforeHook) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/roda/plugins/_symbol_regexp_matchers.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The _symbol_regexp_matchers plugin is designed for internal use by other plugins, 7 | # for the historical behavior of a symbol matching an arbitrary segment by default 8 | # using a regexp. 9 | module SymbolRegexpMatchers 10 | module RequestMethods 11 | private 12 | 13 | # The regular expression to use for matching symbols. By default, any non-empty 14 | # segment matches. 15 | def _match_symbol_regexp(s) 16 | "([^\\/]+)" 17 | end 18 | end 19 | end 20 | 21 | register_plugin(:_symbol_regexp_matchers, SymbolRegexpMatchers) 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/roda/plugins/all_verbs.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The all_verbs plugin adds methods for http verbs other than 7 | # get and post. The following verbs are added, assuming 8 | # rack handles them: delete, head, options, link, patch, put, 9 | # trace, unlink. 10 | # 11 | # These methods operate just like Roda's default get and post 12 | # methods, so using them without any arguments just checks for 13 | # the request method, while using them with any arguments also 14 | # checks that the arguments match the full path. 15 | # 16 | # Example: 17 | # 18 | # plugin :all_verbs 19 | # 20 | # route do |r| 21 | # r.delete do 22 | # # Handle DELETE 23 | # end 24 | # r.put do 25 | # # Handle PUT 26 | # end 27 | # r.patch do 28 | # # Handle PATCH 29 | # end 30 | # end 31 | # 32 | # The verb methods are defined via metaprogramming, so there 33 | # isn't documentation for the individual methods created. 34 | module AllVerbs 35 | module RequestMethods 36 | %w'delete head options link patch put trace unlink'.each do |verb| 37 | if ::Rack::Request.method_defined?("#{verb}?") 38 | class_eval(<<-END, __FILE__, __LINE__+1) 39 | def #{verb}(*args, &block) 40 | _verb(args, &block) if #{verb}? 41 | end 42 | END 43 | end 44 | end 45 | end 46 | end 47 | 48 | register_plugin(:all_verbs, AllVerbs) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/roda/plugins/assume_ssl.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The assume_ssl plugin makes the request ssl? method always return 7 | # true. This is useful when using an SSL-terminating reverse proxy 8 | # that doesn't set the X-Forwarded-Proto or similar header to notify 9 | # Rack that it is forwarding an SSL request. 10 | # 11 | # The sessions and sinatra_helpers plugins that ship with Roda both 12 | # use the ssl? method internally and can be affected by use of the 13 | # plugin. It's recommended that you use this plugin if you are 14 | # using either plugin and an SSL-terminating proxy as described above. 15 | # 16 | # plugin :assume_ssl 17 | module AssumeSSL 18 | module RequestMethods 19 | # Assume all requests are protected by SSL. 20 | def ssl? 21 | true 22 | end 23 | end 24 | end 25 | 26 | register_plugin(:assume_ssl, AssumeSSL) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/roda/plugins/break.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The break plugin supports calling break inside a match block, to 7 | # return from the block and continue in the routing tree, restoring 8 | # the remaining path so that future matchers operating on the path 9 | # operate as expected. 10 | # 11 | # plugin :break 12 | # 13 | # route do |r| 14 | # r.on "foo", :bar do |bar| 15 | # break if bar == 'baz' 16 | # "/foo/#{bar} (not baz)" 17 | # end 18 | # 19 | # r.on "foo/baz" do 20 | # "/foo/baz" 21 | # end 22 | # end 23 | # 24 | # This provides the same basic feature as the pass plugin, but 25 | # uses Ruby's standard control flow primative instead of a 26 | # separate method. 27 | module Break 28 | module RequestMethods 29 | private 30 | 31 | # Handle break inside match blocks, restoring remaining path. 32 | def if_match(_) 33 | rp = @remaining_path 34 | super 35 | ensure 36 | @remaining_path = rp 37 | end 38 | end 39 | end 40 | 41 | register_plugin(:break, Break) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/roda/plugins/default_status.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The default_status plugin accepts a block which should 7 | # return a response status integer. This integer will be used as 8 | # the default response status (usually 200) if the body has been 9 | # written to, and you have not explicitly set a response status. 10 | # 11 | # Example: 12 | # 13 | # # Use 201 default response status for all requests 14 | # plugin :default_status do 15 | # 201 16 | # end 17 | module DefaultStatus 18 | def self.configure(app, &block) 19 | raise RodaError, "default_status plugin requires a block" unless block 20 | if check_arity = app.opts.fetch(:check_arity, true) 21 | unless block.arity == 0 22 | if check_arity == :warn 23 | RodaPlugins.warn "Arity mismatch in block passed to plugin :default_status. Expected Arity 0, but arguments required for #{block.inspect}" 24 | end 25 | b = block 26 | block = lambda{instance_exec(&b)} # Fallback 27 | end 28 | end 29 | app::RodaResponse.send(:define_method, :default_status, &block) 30 | end 31 | end 32 | 33 | register_plugin(:default_status, DefaultStatus) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/roda/plugins/delay_build.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | module DelayBuild 7 | module ClassMethods 8 | # No-op for backwards compatibility 9 | def build! 10 | end 11 | end 12 | end 13 | 14 | # RODA4: Remove plugin 15 | # Only available for backwards compatibility, no longer needed 16 | register_plugin(:delay_build, DelayBuild) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/roda/plugins/delete_empty_headers.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The delete_empty_headers plugin deletes any headers whose 7 | # value is set to the empty string. Because of how default headers are 8 | # set in Roda, if you have a default header but don't want 9 | # to set it for a specific request, you need to use this plugin 10 | # and set the header value to the empty string, and Roda will automatically 11 | # delete the header. 12 | module DeleteEmptyHeaders 13 | module ResponseMethods 14 | # Delete any empty headers when calling finish 15 | def finish 16 | delete_empty_headers(super) 17 | end 18 | 19 | # Delete any empty headers when calling finish_with_body 20 | def finish_with_body(_) 21 | delete_empty_headers(super) 22 | end 23 | 24 | private 25 | 26 | # Delete any empty headers from response 27 | def delete_empty_headers(res) 28 | res[1].delete_if{|_, v| v.is_a?(String) && v.empty?} 29 | res 30 | end 31 | end 32 | end 33 | 34 | register_plugin(:delete_empty_headers, DeleteEmptyHeaders) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/roda/plugins/direct_call.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The direct_call plugin makes the call class method skip the middleware stack 7 | # (app.call will still call the middleware). 8 | # This can be used as an optimization, as the Roda class itself can be used 9 | # as the callable, which is faster than using a lambda. 10 | module DirectCall 11 | def self.configure(app) 12 | app.send(:build_rack_app) 13 | end 14 | 15 | module ClassMethods 16 | # Call the application without middlware. 17 | def call(env) 18 | new(env)._roda_handle_main_route 19 | end 20 | 21 | private 22 | 23 | # If new_api is true, use the receiver as the base rack app for better 24 | # performance. 25 | def base_rack_app_callable(new_api=true) 26 | if new_api 27 | self 28 | else 29 | super 30 | end 31 | end 32 | end 33 | end 34 | 35 | register_plugin(:direct_call, DirectCall) 36 | end 37 | end 38 | 39 | -------------------------------------------------------------------------------- /lib/roda/plugins/disallow_file_uploads.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | raise LoadError, "disallow_file_uploads plugin not supported on Rack <1.6" if Rack.release < '1.6' 4 | 5 | # 6 | class Roda 7 | module RodaPlugins 8 | # The disallow_file_uploads plugin raises a Roda::RodaPlugins::DisallowFileUploads::Error 9 | # if there is an attempt to upload a file. This plugin is useful for applications where 10 | # multipart file uploads are not expected and you want to remove the ability for rack 11 | # to create temporary files. Example: 12 | # 13 | # plugin :disallow_file_uploads 14 | # 15 | # This plugin is only supported on Rack 1.6+. This plugin does not technically 16 | # block users from uploading files, it only blocks the parsing of request bodies containing 17 | # multipart file uploads. So if you do not call +r.POST+ (or something that calls it such as 18 | # +r.params+), then Roda will not attempt to parse the request body, and an exception will not 19 | # be raised. 20 | module DisallowFileUploads 21 | # Exception class used when a multipart file upload is attempted. 22 | class Error < RodaError; end 23 | 24 | NO_TEMPFILE = lambda{|_,_| raise Error, "Support for uploading files has been disabled"} 25 | 26 | module RequestMethods 27 | # HTML escape the input and return the escaped version. 28 | def initialize(_, env) 29 | env['rack.multipart.tempfile_factory'] = NO_TEMPFILE 30 | super 31 | end 32 | end 33 | end 34 | 35 | register_plugin(:disallow_file_uploads, DisallowFileUploads) 36 | end 37 | end 38 | 39 | -------------------------------------------------------------------------------- /lib/roda/plugins/drop_body.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The drop_body plugin automatically drops the body and 7 | # Content-Type/Content-Length headers from the response if 8 | # the response status indicates that the response should 9 | # not include a body (response statuses 100, 101, 102, 204, 10 | # and 304). For response status 205, the body and Content-Type 11 | # headers are dropped, but the Content-length header is set to 12 | # '0' instead of being dropped. 13 | module DropBody 14 | module ResponseMethods 15 | DROP_BODY_STATUSES = [100, 101, 102, 204, 205, 304].freeze 16 | RodaPlugins.deprecate_constant(self, :DROP_BODY_STATUSES) 17 | 18 | DROP_BODY_RANGE = 100..199 19 | private_constant :DROP_BODY_RANGE 20 | 21 | # If the response status indicates a body should not be 22 | # returned, use an empty body and remove the Content-Length 23 | # and Content-Type headers. 24 | def finish 25 | r = super 26 | case r[0] 27 | when DROP_BODY_RANGE, 204, 304 28 | r[2] = EMPTY_ARRAY 29 | h = r[1] 30 | h.delete(RodaResponseHeaders::CONTENT_LENGTH) 31 | h.delete(RodaResponseHeaders::CONTENT_TYPE) 32 | when 205 33 | r[2] = EMPTY_ARRAY 34 | empty_205_headers(r[1]) 35 | end 36 | r 37 | end 38 | end 39 | end 40 | 41 | register_plugin(:drop_body, DropBody) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/roda/plugins/early_hints.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The early_hints plugin allows sending 103 Early Hints responses 7 | # using the rack.early_hints environment variable. 8 | # Early hints allow clients to preload necessary files before receiving 9 | # the response. 10 | module EarlyHints 11 | module InstanceMethods 12 | # Send given hash of Early Hints using the rack.early_hints environment variable, 13 | # currenly only supported by puma. hash given should generally have the single 14 | # key 'Link', and a string or array of strings for each of the early hints. 15 | def send_early_hints(hash) 16 | if eh_proc = env['rack.early_hints'] 17 | eh_proc.call(hash) 18 | end 19 | end 20 | end 21 | end 22 | 23 | register_plugin(:early_hints, EarlyHints) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/roda/plugins/empty_root.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The empty_root plugin makes +r.root+ match both on +/+ and 7 | # on the empty string. This is mostly useful when using multiple 8 | # rack applications, where the initial PATH_INFO has been moved 9 | # to SCRIPT_NAME. For example, if you have the following 10 | # applications: 11 | # 12 | # class App1 < Roda 13 | # on "albums" do 14 | # run App2 15 | # end 16 | # end 17 | # 18 | # class App2 < Roda 19 | # plugin :empty_root 20 | # 21 | # route do |r| 22 | # r.root do 23 | # "root" 24 | # end 25 | # end 26 | # end 27 | # 28 | # Then requests for both +/albums/+ and +/albums+ will return 29 | # "root". Without this plugin loaded into App2, only requests 30 | # for +/albums/+ will return "root", since by default, +r.root+ 31 | # matches only when the current PATH_INFO is +/+ and not when 32 | # it is empty. 33 | module EmptyRoot 34 | module RequestMethods 35 | # Match when the remaining path is the empty string, 36 | # in addition to the default behavior of matching when 37 | # the remaining path is +/+. 38 | def root(&block) 39 | super 40 | if remaining_path.empty? && is_get? 41 | always(&block) 42 | end 43 | end 44 | end 45 | end 46 | 47 | register_plugin(:empty_root, EmptyRoot) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/roda/plugins/erb_h.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'erb/escape' 4 | 5 | # 6 | class Roda 7 | module RodaPlugins 8 | # The erb_h plugin adds an +h+ instance method that will HTML 9 | # escape the input and return it. This is similar to the h 10 | # plugin, but it uses erb/escape to implement the HTML escaping, 11 | # which offers faster performance. 12 | # 13 | # To make sure that this speeds up applications using the h 14 | # plugin, this depends on the h plugin, and overrides the 15 | # h method. 16 | # 17 | # The following example will return "<foo>" as the body. 18 | # 19 | # plugin :erb_h 20 | # 21 | # route do |r| 22 | # h('') 23 | # end 24 | # 25 | # The faster performance offered by the erb_h plugin is due 26 | # to erb/escape avoiding allocations if not needed (returning the 27 | # input object if no escaping is needed). That behavior change 28 | # can cause problems if you mutate the result of the h method 29 | # (which can mutate the input), or mutate the input of the h 30 | # method after calling it (which can mutate the result). 31 | module ErbH 32 | def self.load_dependencies(app) 33 | app.plugin :h 34 | end 35 | 36 | module InstanceMethods 37 | define_method(:h, ERB::Escape.instance_method(:html_escape)) 38 | end 39 | end 40 | 41 | register_plugin(:erb_h, ErbH) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/roda/plugins/filter_common_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The skip_common_logger plugin allows for skipping common_logger logging 7 | # of some requests. You pass a block when loading the plugin, and the 8 | # block will be called before logging each request. The block should return 9 | # whether the request should be logged. 10 | # 11 | # Example: 12 | # 13 | # # Only log server errors 14 | # plugin :filter_common_logger do |result| 15 | # result[0] >= 500 16 | # end 17 | # 18 | # # Don't log requests to certain paths 19 | # plugin :filter_common_logger do |_| 20 | # # Block is called in the same context as the route block 21 | # !request.path.start_with?('/admin/') 22 | # end 23 | module FilterCommonLogger 24 | def self.load_dependencies(app, &_) 25 | app.plugin :common_logger 26 | end 27 | 28 | def self.configure(app, &block) 29 | app.send(:define_method, :_common_log_request?, &block) 30 | app.send(:private, :_common_log_request?) 31 | app.send(:alias_method, :_common_log_request?, :_common_log_request?) 32 | end 33 | 34 | module InstanceMethods 35 | private 36 | 37 | # Log request/response information in common log format to logger. 38 | def _roda_after_90__common_logger(result) 39 | super if result && _common_log_request?(result) 40 | end 41 | end 42 | end 43 | 44 | register_plugin(:filter_common_logger, FilterCommonLogger) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/roda/plugins/hash_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The hash_matcher plugin adds the hash_matcher class method, which 7 | # allows for easily defining hash matchers: 8 | # 9 | # class App < Roda 10 | # hash_matcher(:foo) do |v| 11 | # params['foo'] == v 12 | # end 13 | # 14 | # route do 15 | # r.on foo: 'bar' do 16 | # # matches when param foo has value bar 17 | # end 18 | # end 19 | # end 20 | module HashMatcher 21 | module ClassMethods 22 | # Create a match_#{key} method in the request class using the given 23 | # block, so that using a hash key in a request match method will 24 | # call the block. The block should return nil or false to not 25 | # match, and anything else to match. See the HashMatcher module 26 | # documentation for an example. 27 | def hash_matcher(key, &block) 28 | meth = :"match_#{key}" 29 | self::RodaRequest.send(:define_method, meth, &block) 30 | self::RodaRequest.send(:private, meth) 31 | end 32 | end 33 | end 34 | 35 | register_plugin(:hash_matcher, HashMatcher) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/roda/plugins/heartbeat.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The heartbeat handles heartbeat/status requests. If a request for 7 | # the heartbeat path comes in, a 200 response with a 8 | # text/plain Content-Type and a body of "OK" will be returned. 9 | # The default heartbeat path is "/heartbeat", so to use that: 10 | # 11 | # plugin :heartbeat 12 | # 13 | # You can also specify a custom heartbeat path: 14 | # 15 | # plugin :heartbeat, path: '/status' 16 | module Heartbeat 17 | # Set the heartbeat path to the given path. 18 | def self.configure(app, opts=OPTS) 19 | app.opts[:heartbeat_path] = (opts[:path] || app.opts[:heartbeat_path] || "/heartbeat").dup.freeze 20 | end 21 | 22 | module InstanceMethods 23 | private 24 | 25 | # If the request is for a heartbeat path, return the heartbeat response. 26 | def _roda_before_20__heartbeat 27 | if env['PATH_INFO'] == opts[:heartbeat_path] 28 | response = @_response 29 | response.status = 200 30 | response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain' 31 | response.write 'OK' 32 | throw :halt, response.finish 33 | end 34 | end 35 | end 36 | end 37 | 38 | register_plugin(:heartbeat, Heartbeat) 39 | end 40 | end 41 | 42 | -------------------------------------------------------------------------------- /lib/roda/plugins/hsts.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The hsts plugin allows for easily configuring an appropriate 7 | # Strict-Transport-Security response header for the application: 8 | # 9 | # plugin :hsts 10 | # # Strict-Transport-Security: max-age=63072000; includeSubDomains 11 | # 12 | # plugin :hsts, preload: true 13 | # # Strict-Transport-Security: max-age=63072000; includeSubDomains; preload 14 | # 15 | # plugin :hsts, max_age: 31536000, subdomains: false 16 | # # Strict-Transport-Security: max-age=31536000 17 | module Hsts 18 | # Ensure default_headers plugin is loaded first 19 | def self.load_dependencies(app, opts=OPTS) 20 | app.plugin :default_headers 21 | end 22 | 23 | # Configure the Strict-Transport-Security header. Options: 24 | # :max_age :: Set max-age in seconds (default is 63072000, two years) 25 | # :preload :: Set preload, so the domain can be included in HSTS preload lists 26 | # :subdomains :: Set to false to not set includeSubDomains. By default, 27 | # includeSubDomains is set to enforce HTTPS for subdomains. 28 | def self.configure(app, opts=OPTS) 29 | app.plugin :default_headers, RodaResponseHeaders::STRICT_TRANSPORT_SECURITY => "max-age=#{opts[:max_age]||63072000}#{'; includeSubDomains' unless opts[:subdomains] == false}#{'; preload' if opts[:preload]}".freeze 30 | end 31 | end 32 | 33 | register_plugin(:hsts, Hsts) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/roda/plugins/inject_erb.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The inject_erb plugin allows you to inject content directly 7 | # into the template output: 8 | # 9 | # <% inject_erb("Some HTML Here") %> 10 | # 11 | # This will inject Some HTML Here into the template output, 12 | # even though the tag being used is <% and not <%=. 13 | # 14 | # This method can be used inside methods, such as to wrap calls to 15 | # methods that accept template blocks, to inject code before and after 16 | # the template blocks. 17 | module InjectERB 18 | def self.load_dependencies(app) 19 | app.plugin :render 20 | end 21 | 22 | module InstanceMethods 23 | # Inject into the template output for the template currently being 24 | # rendered. 25 | def inject_erb(value) 26 | instance_variable_get(render_opts[:template_opts][:outvar]) << value.to_s 27 | end 28 | end 29 | end 30 | 31 | register_plugin(:inject_erb, InjectERB) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/roda/plugins/match_hook.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The match_hook plugin adds hooks that are called upon a successful match 7 | # by any of the matchers. The hooks do not take any arguments. If you would 8 | # like hooks that pass the arguments/matchers and values yielded to the route block, 9 | # use the match_hook_args plugin. This uses the match_hook_args plugin internally, 10 | # but doesn't pass the matchers and values yielded. 11 | # 12 | # plugin :match_hook 13 | # 14 | # match_hook do 15 | # logger.debug("#{request.matched_path} matched. #{request.remaining_path} remaining.") 16 | # end 17 | module MatchHook 18 | def self.load_dependencies(app) 19 | app.plugin :match_hook_args 20 | end 21 | 22 | module ClassMethods 23 | # Add a match hook. 24 | def match_hook(&block) 25 | meth = define_roda_method("match_hook", 0, &block) 26 | add_match_hook{|_,_| send(meth)} 27 | nil 28 | end 29 | end 30 | end 31 | 32 | register_plugin :match_hook, MatchHook 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/roda/plugins/padrino_render.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The padrino_render plugin adds rendering support that is 7 | # similar to Padrino's. While not everything Padrino's 8 | # rendering supports is supported by this plugin, it 9 | # currently handles enough to be a drop in replacement for 10 | # some applications. 11 | # 12 | # plugin :padrino_render, views: 'path/2/views' 13 | # 14 | # Most notably, this makes the +render+ method default to 15 | # using the layout, similar to how the +view+ method works 16 | # in the render plugin. If you want to call render and not 17 | # use a layout, you can use the layout: false 18 | # option: 19 | # 20 | # render('test') # layout 21 | # render('test', layout: false) # no layout 22 | # 23 | # Note that this plugin loads the :partials plugin. 24 | module PadrinoRender 25 | # Depend on the render plugin, since this overrides 26 | # some of its methods. 27 | def self.load_dependencies(app, opts=OPTS) 28 | app.plugin :partials, opts 29 | end 30 | 31 | module InstanceMethods 32 | # Call view with the given arguments, so that render 33 | # uses a layout by default. 34 | def render(template, opts=OPTS) 35 | view(template, opts) 36 | end 37 | end 38 | end 39 | 40 | register_plugin(:padrino_render, PadrinoRender) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/roda/plugins/pass.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The pass plugin adds a request +pass+ method to skip the current match 7 | # block as if it did not match. 8 | # 9 | # plugin :pass 10 | # 11 | # route do |r| 12 | # r.on "foo", :bar do |bar| 13 | # r.pass if bar == 'baz' 14 | # "/foo/#{bar} (not baz)" 15 | # end 16 | # 17 | # r.on "foo/baz" do 18 | # "/foo/baz" 19 | # end 20 | # end 21 | module Pass 22 | module RequestMethods 23 | # Skip the current match block as if it did not match. 24 | def pass 25 | throw :pass 26 | end 27 | 28 | private 29 | 30 | # Handle passing inside the match block. 31 | def always 32 | catch(:pass){super} 33 | end 34 | 35 | # Handle passing inside the match block. 36 | def if_match(_) 37 | rp = @remaining_path 38 | ret = catch(:pass){super} 39 | @remaining_path = rp 40 | ret 41 | end 42 | end 43 | end 44 | 45 | register_plugin(:pass, Pass) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/roda/plugins/plain_hash_response_headers.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The plain_hash_response_headers plugin will change Roda to 7 | # use a plain hash for response headers. This is Roda's 8 | # default behavior on Rack 2, but on Rack 3+, Roda defaults 9 | # to using Rack::Headers for response headers for backwards 10 | # compatibility (Rack::Headers automatically lower cases header 11 | # keys). 12 | # 13 | # On Rack 3+, you should use this plugin for better performance 14 | # if you are sure all headers in your application and middleware 15 | # are already lower case (lower case response header keys are 16 | # required by the Rack 3 spec). 17 | module PlainHashResponseHeaders 18 | if defined?(Rack::Headers) && Rack::Headers.is_a?(Class) 19 | module ResponseMethods 20 | private 21 | 22 | # Use plain hash for headers 23 | def _initialize_headers 24 | {} 25 | end 26 | end 27 | end 28 | end 29 | 30 | register_plugin(:plain_hash_response_headers, PlainHashResponseHeaders) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/roda/plugins/r.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The r plugin adds an +r+ instance method that will return the request. 7 | # This allows you to use common Roda idioms such as +r.halt+ and 8 | # +r.redirect+ even when +r+ isn't a local variable in scope. Example: 9 | # 10 | # plugin :r 11 | # 12 | # def foo 13 | # r.redirect "/bar" 14 | # end 15 | # 16 | # route do |r| 17 | # r.get "foo" do 18 | # foo 19 | # end 20 | # r.get "bar" do 21 | # "bar" 22 | # end 23 | # end 24 | module R 25 | module InstanceMethods 26 | # The request object. 27 | def r 28 | @_request 29 | end 30 | end 31 | end 32 | 33 | register_plugin(:r, R) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/roda/plugins/response_request.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The response_request plugin gives the response access to the 7 | # related request instance via the #request method. 8 | # 9 | # Example: 10 | # 11 | # plugin :response_request 12 | module ResponseRequest 13 | module InstanceMethods 14 | # Set the response's request to the current request. 15 | def initialize(env) 16 | super 17 | @_response.request = @_request 18 | end 19 | end 20 | 21 | module ResponseMethods 22 | # The request related to this response. 23 | attr_accessor :request 24 | end 25 | end 26 | 27 | register_plugin(:response_request, ResponseRequest) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/roda/plugins/run_require_slash.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The run_require_slash plugin makes +r.run+ a no-op if the remaining 7 | # path is not empty and does not start with +/+. The Rack SPEC requires that 8 | # +PATH_INFO+ start with a slash if not empty, so this plugin prevents 9 | # dispatching to the application with an environment that would violate the 10 | # Rack SPEC. 11 | # 12 | # You are unlikely to want to use this plugin unless are consuming partial 13 | # segments of the request path, or using the match_affix plugin to change 14 | # how routing is done: 15 | # 16 | # plugin :match_affix, "", /(\/|\z)/ 17 | # route do |r| 18 | # r.on "/a" do 19 | # r.on "b" do 20 | # r.run App 21 | # end 22 | # end 23 | # end 24 | # 25 | # # with run_require_slash: 26 | # # GET /a/b/e => App not dispatched to 27 | # # GET /a/b => App gets "" as PATH_INFO 28 | # 29 | # # with run_require_slash: 30 | # # GET /a/b/e => App gets "e" as PATH_INFO, violating rack SPEC 31 | # # GET /a/b => App gets "" as PATH_INFO 32 | module RunRequireSlash 33 | module RequestMethods 34 | # Calls the given rack app only if the remaining patch is empty or 35 | # starts with a slash. 36 | def run(*) 37 | if @remaining_path.empty? || @remaining_path.start_with?('/') 38 | super 39 | end 40 | end 41 | end 42 | end 43 | 44 | register_plugin(:run_require_slash, RunRequireSlash) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/roda/plugins/slash_path_empty.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The slash_path_empty plugin considers "/" as an empty path, 7 | # in addition to the default of "" being considered an empty 8 | # path. This makes it so +r.is+ without an argument will match 9 | # a path of "/", and +r.is+ and verb methods such as +r.get+ and 10 | # +r.post+ will match if the path is "/" after the arguments 11 | # are processed. This can make it easier to handle applications 12 | # where a trailing "/" in the path should be ignored. 13 | module SlashPathEmpty 14 | module RequestMethods 15 | private 16 | 17 | # Consider the path empty if it is "/". 18 | def empty_path? 19 | super || remaining_path == '/' 20 | end 21 | end 22 | end 23 | 24 | register_plugin(:slash_path_empty, SlashPathEmpty) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/roda/plugins/status_303.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The status_303 plugin sets the default redirect status to be 303 7 | # rather than 302 when the request is not a GET and the 8 | # redirection occurs on an HTTP 1.1 connection as per RFC 7231. 9 | # There are some frontend frameworks that require this behavior. 10 | # 11 | # Example: 12 | # 13 | # plugin :status_303 14 | module Status303 15 | module RequestMethods 16 | 17 | private 18 | 19 | def default_redirect_status 20 | return super if is_get? 21 | 22 | case http_version 23 | when 'HTTP/1.0', 'HTTP/0.9', nil 24 | super 25 | else 26 | 303 27 | end 28 | end 29 | end 30 | end 31 | 32 | register_plugin(:status_303, Status303) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/roda/plugins/strip_path_prefix.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The strip_path_prefix plugin makes Roda strip a given prefix off internal absolute paths, 7 | # turning them to relative paths. Roda by default stores internal paths as absolute paths. 8 | # The main reason to use this plugin is when the internal absolute path could change at 9 | # runtime, either due to a symlink change or chroot call, or you really want to use 10 | # relative paths instead of absolute paths. 11 | # 12 | # Examples: 13 | # 14 | # plugin :strip_path_prefix # Defaults to Dir.pwd 15 | # plugin :strip_path_prefix, File.dirname(Dir.pwd) 16 | module StripPathPrefix 17 | # Set the regexp to use when stripping prefixes from internal paths. 18 | def self.configure(app, prefix=Dir.pwd) 19 | prefix += '/' unless prefix=~ /\/\z/ 20 | app.opts[:strip_path_prefix] = /\A#{Regexp.escape(prefix)}/ 21 | end 22 | 23 | module ClassMethods 24 | # Strip the path prefix from the gien path if it starts with the prefix. 25 | def expand_path(path, root=opts[:root]) 26 | super.sub(opts[:strip_path_prefix], '') 27 | end 28 | end 29 | end 30 | 31 | register_plugin(:strip_path_prefix, StripPathPrefix) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/roda/plugins/symbol_status.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'rack/utils' 4 | 5 | class Roda 6 | module RodaPlugins 7 | # The symbol_status plugin patches the +status=+ response method to 8 | # accept the status name as a symbol. If given an integer value, 9 | # the default behaviour is used. 10 | # 11 | # Examples: 12 | # r.is "needs_authorization" do 13 | # response.status = :unauthorized 14 | # end 15 | # r.is "nothing" do 16 | # response.status = :no_content 17 | # end 18 | # 19 | # The conversion is done through Rack::Utils.status_code. 20 | module SymbolStatus 21 | module ResponseMethods 22 | # Sets the response status code by fixnum or symbol name 23 | def status=(code) 24 | code = Rack::Utils.status_code(code) if code.is_a?(Symbol) 25 | super(code) 26 | end 27 | end 28 | end 29 | 30 | register_plugin(:symbol_status, SymbolStatus) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/roda/plugins/symbol_views.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | # 4 | class Roda 5 | module RodaPlugins 6 | # The symbol_views plugin allows match blocks to return 7 | # symbols, and consider those symbols as views to use for the 8 | # response body. So you can take code like: 9 | # 10 | # r.root do 11 | # view :index 12 | # end 13 | # r.is "foo" do 14 | # view :foo 15 | # end 16 | # 17 | # and DRY it up: 18 | # 19 | # r.root do 20 | # :index 21 | # end 22 | # r.is "foo" do 23 | # :foo 24 | # end 25 | module SymbolViews 26 | def self.load_dependencies(app) 27 | app.plugin :custom_block_results 28 | end 29 | 30 | def self.configure(app) 31 | app.opts[:custom_block_results][Symbol] = :view 32 | end 33 | end 34 | 35 | register_plugin(:symbol_views, SymbolViews) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/roda/plugins/unescape_path.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'rack/utils' 4 | 5 | # 6 | class Roda 7 | module RodaPlugins 8 | # The unescape_path plugin decodes a URL-encoded path 9 | # before routing. This fixes routing when the slashes 10 | # are URL-encoded as %2f and returns decoded parameters 11 | # when matched by symbols or regexps. 12 | # 13 | # plugin :unescape_path 14 | # 15 | # route do |r| 16 | # # Assume /b/a URL encoded at %2f%62%2f%61 17 | # r.on :x, /(.)/ do |*x| 18 | # # x => ['b', 'a'] 19 | # end 20 | # end 21 | module UnescapePath 22 | module RequestMethods 23 | # Make sure the matched path calculation handles the unescaping 24 | # of the remaining path. 25 | def matched_path 26 | e = @env 27 | Rack::Utils.unescape(e["SCRIPT_NAME"] + e["PATH_INFO"]).chomp(@remaining_path) 28 | end 29 | 30 | private 31 | 32 | # Unescape the path. 33 | def _remaining_path(env) 34 | Rack::Utils.unescape(super) 35 | end 36 | end 37 | end 38 | 39 | register_plugin(:unescape_path, UnescapePath) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/roda/version.rb: -------------------------------------------------------------------------------- 1 | class Roda 2 | # The major version of Roda, updated only for major changes that are 3 | # likely to require modification to Roda apps. 4 | RodaMajorVersion = 3 5 | 6 | # The minor version of Roda, updated for new feature releases of Roda. 7 | RodaMinorVersion = 92 8 | 9 | # The patch version of Roda, updated only for bug fixes from the last 10 | # feature release. 11 | RodaPatchVersion = 0 12 | 13 | # The full version of Roda as a string. 14 | RodaVersion = "#{RodaMajorVersion}.#{RodaMinorVersion}.#{RodaPatchVersion}".freeze 15 | 16 | # The full version of Roda as a number (3.7.0 => 30070) 17 | RodaVersionNumber = RodaMajorVersion*10000 + RodaMinorVersion*10 + RodaPatchVersion 18 | end 19 | -------------------------------------------------------------------------------- /roda.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("../lib/roda/version", __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "roda" 5 | s.version = Roda::RodaVersion.dup 6 | s.summary = "Routing tree web toolkit" 7 | s.authors = ["Jeremy Evans"] 8 | s.email = ["code@jeremyevans.net"] 9 | s.homepage = "https://roda.jeremyevans.net" 10 | s.license = "MIT" 11 | s.required_ruby_version = ">= 1.9.2" 12 | s.metadata = { 13 | 'bug_tracker_uri' => 'https://github.com/jeremyevans/roda/issues', 14 | 'changelog_uri' => 'https://roda.jeremyevans.net/rdoc/files/CHANGELOG.html', 15 | 'documentation_uri' => 'https://roda.jeremyevans.net/documentation.html', 16 | 'mailing_list_uri' => 'https://github.com/jeremyevans/roda/discussions', 17 | "source_code_uri" => "https://github.com/jeremyevans/roda" 18 | } 19 | 20 | s.files = %w'MIT-LICENSE' + Dir['lib/**/*.rb'] 21 | s.extra_rdoc_files = %w'MIT-LICENSE' 22 | 23 | s.add_dependency "rack" 24 | s.add_development_dependency "rake" 25 | s.add_development_dependency "minitest", ">= 5.7.0" 26 | s.add_development_dependency "minitest-hooks" 27 | s.add_development_dependency "minitest-global_expectations" 28 | s.add_development_dependency "tilt" 29 | s.add_development_dependency "erubi" 30 | s.add_development_dependency "rack_csrf" 31 | s.add_development_dependency "json" 32 | s.add_development_dependency "mail" 33 | end 34 | -------------------------------------------------------------------------------- /spec/all.rb: -------------------------------------------------------------------------------- 1 | (Dir['./spec/*_spec.rb'] + Dir['./spec/plugin/*_spec.rb']).each{|f| require f} 2 | -------------------------------------------------------------------------------- /spec/assets/css/app.html: -------------------------------------------------------------------------------- 1 | body { color: red; } 2 | -------------------------------------------------------------------------------- /spec/assets/css/app.str: -------------------------------------------------------------------------------- 1 | body { color: red; } 2 | -------------------------------------------------------------------------------- /spec/assets/css/import.str: -------------------------------------------------------------------------------- 1 | #{File.binread(File.join(File.dirname(__FILE__), 'importdep.str'))} 2 | -------------------------------------------------------------------------------- /spec/assets/css/no_access.css: -------------------------------------------------------------------------------- 1 | no access 2 | -------------------------------------------------------------------------------- /spec/assets/css/raw.css: -------------------------------------------------------------------------------- 1 | body { color: blue; } 2 | -------------------------------------------------------------------------------- /spec/assets/js/head/app.js: -------------------------------------------------------------------------------- 1 | console.log('test') 2 | -------------------------------------------------------------------------------- /spec/assets/js/head/comment_1.js: -------------------------------------------------------------------------------- 1 | // test -------------------------------------------------------------------------------- /spec/assets/js/head/comment_2.js: -------------------------------------------------------------------------------- 1 | /* 2 | a = 1; 3 | */ 4 | -------------------------------------------------------------------------------- /spec/autoload_hash_branches/a.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :a 2 | $roda_app.hash_branch('a'){|r| r.hash_branches; 'a'} 3 | -------------------------------------------------------------------------------- /spec/autoload_hash_branches/a/c.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :a_c 2 | $roda_app.hash_branch('/a', 'c'){|r| 'a-c'} 3 | -------------------------------------------------------------------------------- /spec/autoload_hash_branches/a/d.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :a_d 2 | $roda_app.hash_branch('/a', 'd'){|r| 'a-d'} 3 | -------------------------------------------------------------------------------- /spec/autoload_hash_branches/a/e.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :a_e 2 | -------------------------------------------------------------------------------- /spec/autoload_hash_branches/b.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :b 2 | $roda_app.hash_branch('b'){|r| 'b'} 3 | -------------------------------------------------------------------------------- /spec/autoload_named_routes/a.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :a 2 | $roda_app.route(:a) do |r| 3 | r.on('c'){r.route(:c, :a)} 4 | r.on('d'){r.route(:d, :a)} 5 | r.on('e'){r.route(:e, :a)} 6 | 'a' 7 | end 8 | -------------------------------------------------------------------------------- /spec/autoload_named_routes/a/c.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :a_c 2 | $roda_app.route(:c, :a){|r| 'a-c'} 3 | -------------------------------------------------------------------------------- /spec/autoload_named_routes/a/d.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :a_d 2 | $roda_app.route(:d, :a){|r| 'a-d'} 3 | -------------------------------------------------------------------------------- /spec/autoload_named_routes/a/e.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :a_e 2 | -------------------------------------------------------------------------------- /spec/autoload_named_routes/b.rb: -------------------------------------------------------------------------------- 1 | $roda_app.opts[:loaded] << :b 2 | $roda_app.route(:b){|r| 'b'} 3 | -------------------------------------------------------------------------------- /spec/cache_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "Roda::RodaCache" do 4 | before do 5 | @cache = Roda::RodaCache.new 6 | end 7 | 8 | it "should provide a hash like interface" do 9 | @cache[1].must_be_nil 10 | @cache[1] = 2 11 | @cache[1].must_equal 2 12 | end 13 | 14 | it "should have dup return a copy of the cache" do 15 | @cache[1].must_be_nil 16 | @cache[1] = 2 17 | 18 | cache = @cache.dup 19 | @cache[2] = 3 20 | cache[3] = 4 21 | 22 | cache[1].must_equal 2 23 | cache[2].must_be_nil 24 | cache[3].must_equal 4 25 | 26 | @cache[1].must_equal 2 27 | @cache[2].must_equal 3 28 | @cache[3].must_be_nil 29 | end 30 | 31 | it "should have freeze return a frozen hash" do 32 | v = @cache.freeze 33 | v.must_equal({}) 34 | v.must_be_instance_of(Hash) 35 | end 36 | end 37 | 38 | -------------------------------------------------------------------------------- /spec/composition_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "r.run" do 4 | it "should allow composition of apps" do 5 | a = app do |r| 6 | r.on "services", :id do |id| 7 | "View #{id}" 8 | end 9 | end 10 | 11 | app(:new) do |r| 12 | r.on "provider" do 13 | r.run a 14 | end 15 | end 16 | 17 | body("/provider/services/101").must_equal 'View 101' 18 | end 19 | 20 | it "modifies SCRIPT_NAME/PATH_INFO when calling run" do 21 | a = app{|r| "#{r.script_name}|#{r.path_info}"} 22 | app{|r| r.on("a"){r.run a}} 23 | body("/a/b").must_equal "/a|/b" 24 | end 25 | 26 | it "restores SCRIPT_NAME/PATH_INFO before returning from run" do 27 | a = app{|r| "#{r.script_name}|#{r.path_info}"} 28 | x = nil 29 | app do |r| 30 | s = catch(:halt){r.on("a"){r.run a}} 31 | x = s[2] 32 | x.close if x.respond_to?(:close) 33 | "#{r.script_name}|#{r.path_info}" 34 | end 35 | body("/a/b").must_equal "|/a/b" 36 | x = '/a|/b' 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/env_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "Roda#env" do 4 | it "should return the environment" do 5 | app do |r| 6 | env['PATH_INFO'] 7 | end 8 | 9 | body("/foo").must_equal "/foo" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/freeze_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "Roda.freeze" do 4 | before do 5 | app{'a'}.freeze 6 | end 7 | 8 | it "should result in a working application" do 9 | body.must_equal 'a' 10 | end 11 | 12 | it "should not break if called more than once" do 13 | app.freeze 14 | body.must_equal 'a' 15 | end 16 | 17 | it "should make opts not be modifiable after calling finalize!" do 18 | proc{app.opts[:foo] = 'bar'}.must_raise 19 | end 20 | 21 | it "should make use and route raise errors" do 22 | proc{app.use Class.new}.must_raise 23 | proc{app.route{}}.must_raise 24 | end 25 | 26 | it "should make plugin raise errors" do 27 | proc{app.plugin Module.new}.must_raise 28 | end 29 | 30 | it "should make subclassing raise errors" do 31 | proc{Class.new(app)}.must_raise 32 | end 33 | 34 | it "should freeze app" do 35 | app.frozen?.must_equal true 36 | end 37 | 38 | it "should work after adding middleware" do 39 | app(:bare) do 40 | use(Class.new do 41 | def initialize(app) @app = app end 42 | def call(env) @app.call(env) end 43 | end) 44 | route do |_| 45 | 'a' 46 | end 47 | end 48 | 49 | app.freeze 50 | body.must_equal 'a' 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/opts_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "opts" do 4 | it "is inheritable and allows overriding" do 5 | c = Class.new(Roda) 6 | c.opts[:foo] = "bar" 7 | c.opts[:foo].must_equal "bar" 8 | 9 | sc = Class.new(c) 10 | sc.opts[:foo].must_equal "bar" 11 | 12 | sc.opts[:foo] = "baz" 13 | sc.opts[:foo].must_equal "baz" 14 | c.opts[:foo].must_equal "bar" 15 | end 16 | 17 | it "should be available as an instance methods" do 18 | app(:bare) do 19 | opts[:hello] = "Hello World" 20 | 21 | route do |r| 22 | r.on do 23 | opts[:hello] 24 | end 25 | end 26 | end 27 | 28 | body.must_equal "Hello World" 29 | end 30 | 31 | it "should only shallow clone by default" do 32 | c = Class.new(Roda) 33 | c.opts[:foo] = "bar".dup 34 | c.opts[:foo].must_equal "bar" 35 | 36 | sc = Class.new(c) 37 | sc.opts[:foo].replace("baz") 38 | 39 | sc.opts[:foo].must_equal "baz" 40 | c.opts[:foo].must_equal "baz" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/plugin/Integer_matcher_max_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "Integer_matcher_max plugin" do 4 | it "matches values up to 2**63-1 by default" do 5 | app(:Integer_matcher_max) do |r| 6 | r.is Integer do |i| 7 | i.to_s 8 | end 9 | end 10 | 11 | max = (2**63-1).to_s 12 | body("/0").must_equal '0' 13 | body("/#{max}").must_equal max 14 | body("/#{max.next}").must_equal '' 15 | end 16 | 17 | it "matches configured values if an argument is provided" do 18 | app(:bare) do 19 | plugin :Integer_matcher_max, 2**64-1 20 | route do |r| 21 | r.is Integer do |i| 22 | i.to_s 23 | end 24 | end 25 | end 26 | 27 | max = (2**64-1).to_s 28 | body("/0").must_equal '0' 29 | body("/#{max}").must_equal max 30 | body("/#{max.next}").must_equal '' 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/plugin/_after_hook_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "deprecated _after_hook plugin" do 4 | it "shouldn't break things" do 5 | x = [] 6 | app(:_after_hook) do |r| 7 | x << 0 8 | 'a' 9 | end 10 | @app.send(:include, Module.new do 11 | define_method(:_roda_after_00_test){|_| x << 1} 12 | private :_roda_after_00_test 13 | end) 14 | 15 | body.must_equal 'a' 16 | x.must_equal [0, 1] 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /spec/plugin/additional_render_engines_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | begin 4 | require 'tilt' 5 | require 'tilt/erb' 6 | require 'tilt/string' 7 | require_relative '../../lib/roda/plugins/render' 8 | rescue LoadError 9 | warn "tilt not installed, skipping render plugin test" 10 | else 11 | describe "additional_render_engines plugin" do 12 | it "supports additional render engines" do 13 | app(:bare) do 14 | plugin :render, :views=>'spec/views' 15 | plugin :additional_render_engines, ['str', 'html'] 16 | route do |r| 17 | render(r.remaining_path[1, 1000]).strip 18 | end 19 | end 20 | 21 | body('/a').must_equal 'a' 22 | body('/a1').must_equal 'a1-str' 23 | body('/a2').must_equal 'a2-html' 24 | body('/a3').must_equal 'a3-erb' 25 | 26 | proc{body('/nonexistent')}.must_raise Errno::ENOENT 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/plugin/all_verbs_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "all_verbs plugin" do 4 | it "adds method for each http verb" do 5 | app(:all_verbs) do |r| 6 | r.delete{'d'} 7 | r.head{''} 8 | r.options{'o'} 9 | r.patch{'pa'} 10 | r.put{'pu'} 11 | r.trace{'t'} 12 | if Rack::Request.method_defined?(:link?) 13 | r.link{'l'} 14 | r.unlink{'u'} 15 | end 16 | end 17 | 18 | body('REQUEST_METHOD'=>'DELETE').must_equal 'd' 19 | body('REQUEST_METHOD'=>'HEAD').must_equal '' 20 | body('REQUEST_METHOD'=>'OPTIONS').must_equal 'o' 21 | body('REQUEST_METHOD'=>'PATCH').must_equal 'pa' 22 | body('REQUEST_METHOD'=>'PUT').must_equal 'pu' 23 | body('REQUEST_METHOD'=>'TRACE').must_equal 't' 24 | if Rack::Request.method_defined?(:link?) 25 | body('REQUEST_METHOD'=>'LINK').must_equal 'l' 26 | body('REQUEST_METHOD'=>'UNLINK').must_equal 'u' 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/plugin/assume_ssl_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "assume_ssl plugin" do 4 | it "makes r.ssl? always return true" do 5 | app(:assume_ssl) do |r| 6 | r.ssl?.to_s 7 | end 8 | 9 | body.must_equal 'true' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/plugin/backtracking_array_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "backtracking_array plugin" do 4 | it "backtracks to next entry in array if later matcher fails" do 5 | app(:backtracking_array) do |r| 6 | r.is %w'a a/b' do |id| 7 | id 8 | end 9 | 10 | r.is %w'c c/d', %w'd e' do |a, b| 11 | "#{a}-#{b}" 12 | end 13 | 14 | r.is [%w'f f/g', %w'g g/h'] do |id| 15 | id 16 | end 17 | end 18 | 19 | status.must_equal 404 20 | 21 | body("/a").must_equal 'a' 22 | body("/a/b").must_equal 'a/b' 23 | status("/a/b/").must_equal 404 24 | 25 | body("/c/d").must_equal 'c-d' 26 | body("/c/e").must_equal 'c-e' 27 | body("/c/d/d").must_equal 'c/d-d' 28 | body("/c/d/e").must_equal 'c/d-e' 29 | status("/c/d/").must_equal 404 30 | 31 | body("/f").must_equal 'f' 32 | body("/f/g").must_equal 'f/g' 33 | body("/g").must_equal 'g' 34 | body("/g/h").must_equal 'g/h' 35 | status("/f/g/").must_equal 404 36 | status("/g/h/").must_equal 404 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/plugin/break_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "break plugin" do 4 | it "skips the current block if break is called" do 5 | app(:break) do |r| 6 | r.root do 7 | break if env['FOO'] == 'true' 8 | 'root' 9 | end 10 | 11 | r.on :id do |id| 12 | break if id == 'foo' 13 | id 14 | end 15 | 16 | r.on :x, :y do |x, y| 17 | x + y 18 | end 19 | end 20 | 21 | body.must_equal 'root' 22 | status('FOO'=>'true').must_equal 404 23 | body("/a").must_equal 'a' 24 | body("/a/b").must_equal 'a' 25 | body("/foo/a").must_equal 'fooa' 26 | body("/foo/a/b").must_equal 'fooa' 27 | status("/foo").must_equal 404 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/plugin/custom_block_results_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "custom_block_results plugin" do 4 | before do 5 | app(:custom_block_results) do 6 | :sym 7 | end 8 | end 9 | 10 | it "should handle classes" do 11 | 2.times do 12 | @app.handle_block_result(Symbol) do |s| 13 | "v#{s}" 14 | end 15 | body.must_equal "vsym" 16 | end 17 | end 18 | 19 | it "should handle blocks that do not return strings" do 20 | 2.times do 21 | @app.handle_block_result(Symbol) do |s| 22 | response.status = 201 23 | end 24 | status.must_equal 201 25 | body.must_be_empty 26 | end 27 | end 28 | 29 | it "should handle other objects supporting ===" do 30 | @app.handle_block_result(/sy/) do |s| 31 | "x#{s}" 32 | end 33 | body.must_equal "xsym" 34 | end 35 | 36 | it "should work in subclasses" do 37 | @app = Class.new(@app) 38 | @app.handle_block_result(/sy/) do |s| 39 | "x#{s}" 40 | end 41 | body.must_equal "xsym" 42 | end 43 | 44 | it "should handle frozen applications" do 45 | @app.freeze 46 | proc do 47 | @app.handle_block_result(Symbol){|s| } 48 | end.must_raise 49 | end 50 | 51 | it "should still raise for unhandled types" do 52 | proc{body}.must_raise Roda::RodaError 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/plugin/delay_build_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "delay_build plugin" do 4 | it "does not build rack app until app is called" do 5 | app(:delay_build){"a"} 6 | app.instance_variable_get(:@app).must_be_nil 7 | body.must_equal "a" 8 | # Work around minitest bug 9 | refute_equal app.instance_variable_get(:@app), nil 10 | end 11 | 12 | it "supports the build! method for backwards compatibility" do 13 | app(:delay_build){"a"} 14 | body.must_equal "a" 15 | c = Class.new do 16 | def initialize(_) end 17 | def call(_) [200, {}, ["b"]] end 18 | end 19 | app.use c 20 | app.build! 21 | body.must_equal "b" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/plugin/delegate_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "delegate plugin" do 4 | it "adds request_delegate and response_delegate class methods for delegating" do 5 | app(:bare) do 6 | plugin :delegate 7 | request_delegate :root 8 | response_delegate :headers 9 | 10 | def self.a; 'foo'; end 11 | class_delegate :a 12 | 13 | route do 14 | root do 15 | headers[RodaResponseHeaders::CONTENT_TYPE] = a 16 | end 17 | end 18 | end 19 | 20 | header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'foo' 21 | status('/foo').must_equal 404 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/plugin/delete_empty_headers_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "delete_empty_headers plugin" do 4 | it "automatically deletes headers that are empty" do 5 | app(:delete_empty_headers) do |r| 6 | response['foo'] = '' 7 | response[RodaResponseHeaders::CONTENT_TYPE] = '' 8 | response[RodaResponseHeaders::CONTENT_LENGTH] = '' 9 | response['bar'] = '1' 10 | 'a' 11 | end 12 | 13 | req[1].must_equal('bar'=>'1') 14 | end 15 | 16 | it "is called when finishing with a body" do 17 | app(:delete_empty_headers) do |r| 18 | response['foo'] = '' 19 | response[RodaResponseHeaders::CONTENT_TYPE] = '' 20 | response[RodaResponseHeaders::CONTENT_LENGTH] = '' 21 | response['bar'] = '1' 22 | r.halt response.finish_with_body(['a']) 23 | end 24 | 25 | req[1].must_equal('bar'=>'1') 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/plugin/direct_call_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "direct_call plugin" do 4 | it "should have .call skip middleware" do 5 | app{'123'} 6 | app.use(Class.new do 7 | def initialize(_) end 8 | def call(env) [200, {}, ['321']] end 9 | end) 10 | body.must_equal '321' 11 | app.plugin :direct_call 12 | body.must_equal '123' 13 | end 14 | 15 | deprecated "should work when #call is overridden" do 16 | app.class_eval do 17 | def call; super end 18 | route{'123'} 19 | end 20 | app.use(Class.new do 21 | def initialize(_) end 22 | def call(env) [200, {}, ['321']] end 23 | end) 24 | body.must_equal '321' 25 | app.plugin :direct_call 26 | body.must_equal '123' 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/plugin/disallow_file_uploads_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | begin 4 | require_relative '../../lib/roda/plugins/disallow_file_uploads' 5 | rescue LoadError 6 | warn "Rack #{Rack.release} used, skipping disallow_file_uploads plugin test" 7 | else 8 | describe "disallow_file_uploads plugin" do 9 | it "disallows the uploading of files" do 10 | app do |r| 11 | r.params['foo'][:tempfile].read 12 | end 13 | 14 | request_body = rack_input("------WebKitFormBoundarymwHIM9XjTTVHn3YP\r\nContent-Disposition: form-data; name=\"foo\"; filename=\"bar.txt\"\r\nContent-Type: text/plain\r\n\r\nfoo\n\r\n------WebKitFormBoundarymwHIM9XjTTVHn3YP--\r\n") 15 | 16 | h = { 17 | 'rack.input'=>request_body, 18 | 'CONTENT_TYPE'=>'multipart/form-data; boundary=----WebKitFormBoundarymwHIM9XjTTVHn3YP', 19 | 'CONTENT_LENGTH'=>'184', 20 | 'REQUEST_METHOD'=>'POST' 21 | } 22 | body(h.dup).must_equal "foo\n" 23 | app.plugin :disallow_file_uploads 24 | request_body.rewind 25 | proc{body(h.dup)}.must_raise Roda::RodaPlugins::DisallowFileUploads::Error 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/plugin/drop_body_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "drop_body plugin" do 4 | it "automatically drops body and Content-Type/Content-Length headers for responses without a body" do 5 | app(:drop_body) do |r| 6 | response.status = r.path[1, 1000].to_i 7 | response.write('a') 8 | end 9 | 10 | [100 + rand(100), 204, 304].each do |i| 11 | path = "/#{i.to_s}" 12 | body(path).must_equal '' 13 | header(RodaResponseHeaders::CONTENT_TYPE, path).must_be_nil 14 | header(RodaResponseHeaders::CONTENT_LENGTH, path).must_be_nil 15 | end 16 | 17 | body('/205').must_equal '' 18 | header(RodaResponseHeaders::CONTENT_TYPE, '/205').must_be_nil 19 | if Rack.release < '2.0.2' 20 | header(RodaResponseHeaders::CONTENT_LENGTH, '/205').must_be_nil 21 | else 22 | header(RodaResponseHeaders::CONTENT_LENGTH, '/205').must_equal '0' 23 | end 24 | 25 | body('/200').must_equal 'a' 26 | header(RodaResponseHeaders::CONTENT_TYPE, '/200').must_equal 'text/html' 27 | header(RodaResponseHeaders::CONTENT_LENGTH, '/200').must_equal '1' 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/plugin/early_hints_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "early_hints plugin" do 4 | it "allows sending early hints to rack.early_hints" do 5 | queue = [] 6 | app(:early_hints) do |r| 7 | send_early_hints('link'=>'; rel=preload; as=script') 8 | queue << 'OK' 9 | 'OK' 10 | end 11 | 12 | body.must_equal 'OK' 13 | queue.must_equal ['OK'] 14 | 15 | queue = [] 16 | body('rack.early_hints'=>proc{|h| queue << h}).must_equal 'OK' 17 | queue.must_equal [{'link'=>'; rel=preload; as=script'}, 'OK'] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/plugin/empty_root_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "empty_root plugin" do 4 | it "makes root match on emtpy path" do 5 | app(:empty_root) do |r| 6 | r.root{"root"} 7 | "notroot" 8 | end 9 | 10 | body.must_equal 'root' 11 | unless_lint do 12 | body("").must_equal 'root' 13 | end 14 | body("/a").must_equal 'notroot' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/plugin/environments_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "environments plugin" do 4 | before do 5 | app 6 | app.plugin :environments, :development 7 | end 8 | 9 | it "adds environment accessor for getting/setting the environment" do 10 | app.environment.must_equal :development 11 | app.environment = :test 12 | app.environment.must_equal :test 13 | 14 | app.plugin :environments, :production 15 | app.environment.must_equal :production 16 | end 17 | 18 | it "adds predicates for testing the environment" do 19 | app.development?.must_equal true 20 | app.test?.must_equal false 21 | app.production?.must_equal false 22 | end 23 | 24 | it "adds configure method which yields if no arguments are given or an environment matches" do 25 | a = [] 26 | app.configure{a << 1} 27 | app.configure(:development){|ap| a << ap} 28 | app.configure(:test, :production){a << 2} 29 | a.must_equal [1, app] 30 | end 31 | 32 | it "defaults environment to RACK_ENV" do 33 | with_rack_env('test') do 34 | app(:environments){} 35 | end 36 | app.test?.must_equal true 37 | app.development?.must_equal false 38 | app(:environments){} 39 | app.test?.must_equal false 40 | app.development?.must_equal true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/plugin/erb_h_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | begin 4 | require 'erb/escape' 5 | rescue LoadError 6 | #warn "erb/escape not installed, skipping erb_h plugin test" 7 | else 8 | describe "erb_h plugin" do 9 | it "adds h method for html escaping" do 10 | app(:erb_h) do |r| 11 | h("
") + h(:form) + h("test&<>/'") 12 | end 13 | end 14 | 15 | it "does not allocate object if escaping not needed" do 16 | app(:erb_h) do |r| 17 | s1 = 'a' 18 | s2 = h(s1) 19 | "#{s1}-#{s2}-#{s1.equal?(s2)}" 20 | end 21 | 22 | body.must_equal 'a-a-true' 23 | end 24 | 25 | it "works even if loading h plugin after" do 26 | app(:bare) do 27 | plugin :erb_h 28 | plugin :h 29 | 30 | route do |r| 31 | r.get 'a' do 32 | s1 = 'a' 33 | s2 = h(s1) 34 | "#{s1}-#{s2}-#{s1.equal?(s2)}" 35 | end 36 | 37 | h("") + h(:form) + h("test&<>/'") 38 | end 39 | end 40 | 41 | body.must_equal '<form>formtest&<>/'' 42 | body('/a').must_equal 'a-a-true' 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/plugin/filter_common_logger_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "filter_common_logger plugin" do 4 | it 'allows skipping logging of certain requests' do 5 | logger = rack_input 6 | app(:bare) do 7 | plugin :common_logger, logger 8 | plugin :filter_common_logger do |result| 9 | return false if result[0] >= 300 && result[0] < 400 10 | return false if request.path_info.start_with?('/foo/') 11 | true 12 | end 13 | route do |r| 14 | r.on 'foo' do 15 | 'aa' 16 | end 17 | 18 | r.on 'redir' do 19 | r.redirect '/' 20 | end 21 | 22 | r.on 'err' do 23 | raise 'foo' 24 | end 25 | 26 | 'bbb' 27 | end 28 | end 29 | 30 | body("HTTP_VERSION"=>'HTTP/1.0').must_equal 'bbb' 31 | logger.rewind 32 | logger.read.must_match(/\A- - - \[\d\d\/[A-Z][a-z]{2}\/\d\d\d\d:\d\d:\d\d:\d\d [-+]\d\d\d\d\] "GET \/ HTTP\/1.0" 200 3 0.\d\d\d\d\n\z/) 33 | 34 | logger.rewind 35 | logger.truncate(0) 36 | body('/foo/bar').must_equal 'aa' 37 | logger.rewind 38 | logger.read.must_be_empty 39 | 40 | logger.rewind 41 | logger.truncate(0) 42 | body('/redir').must_equal '' 43 | logger.rewind 44 | logger.read.must_be_empty 45 | 46 | logger.rewind 47 | logger.truncate(0) 48 | proc do 49 | body('/err') 50 | end.must_raise(RuntimeError) 51 | logger.rewind 52 | logger.read.must_be_empty 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/plugin/h_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "h plugin" do 4 | it "adds h method for html escaping" do 5 | app(:h) do |r| 6 | h("") + h(:form) + h("test&<>/'") 7 | end 8 | 9 | body.must_equal '<form>formtest&<>/'' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/plugin/hash_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "hash_matcher plugin" do 4 | it "should enable the handling of arbitrary hash keys" do 5 | app(:bare) do 6 | plugin :hash_matcher 7 | hash_matcher(:foos){|v| consume(self.class.cached_matcher(:"foos-#{v}"){/((?:foo){#{v}})/})} 8 | route do |r| 9 | r.is :foos=>1 do |f| 10 | "1#{f}" 11 | end 12 | r.is :foos=>2 do |f| 13 | "2#{f}" 14 | end 15 | r.is :foos=>3 do |f| 16 | "3#{f}" 17 | end 18 | end 19 | end 20 | 21 | body("/foo").must_equal '1foo' 22 | body("/foofoo").must_equal '2foofoo' 23 | body("/foofoofoo").must_equal '3foofoofoo' 24 | status("/foofoofoofoo").must_equal 404 25 | status.must_equal 404 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/plugin/head_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "head plugin" do 4 | it "considers HEAD requests as GET requests which return no body" do 5 | app(:head) do |r| 6 | r.root do 7 | 'root' 8 | end 9 | 10 | r.get 'a' do 11 | 'a' 12 | end 13 | 14 | r.is 'b', :method=>[:get, :post] do 15 | 'b' 16 | end 17 | end 18 | 19 | s, h, b = req 20 | s.must_equal 200 21 | h[RodaResponseHeaders::CONTENT_LENGTH].must_equal '4' 22 | b.must_equal ['root'] 23 | 24 | s, h, b = req('REQUEST_METHOD' => 'HEAD') 25 | s.must_equal 200 26 | h[RodaResponseHeaders::CONTENT_LENGTH].must_equal '4' 27 | b.must_equal [] 28 | 29 | body('/a').must_equal 'a' 30 | status('/a', 'REQUEST_METHOD' => 'HEAD').must_equal 200 31 | 32 | body('/b').must_equal 'b' 33 | status('/b', 'REQUEST_METHOD' => 'HEAD').must_equal 200 34 | end 35 | 36 | it "releases resources via body.close" do 37 | body = rack_input('') 38 | app(:head) do |r| 39 | r.root do 40 | r.halt [ 200, {}, body ] 41 | end 42 | end 43 | s, _, b = req('REQUEST_METHOD' => 'HEAD') 44 | s.must_equal 200 45 | res = String.new 46 | unless_lint do 47 | body.closed?.must_equal false 48 | end 49 | b.each { |buf| res << buf } 50 | unless_lint do 51 | b.close 52 | end 53 | body.closed?.must_equal true 54 | res.must_equal '' 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/plugin/hsts_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "default_headers plugin" do 4 | def app(opts={}) 5 | super(:bare) do 6 | plugin :hsts, opts 7 | route do |r| 8 | '' 9 | end 10 | end 11 | end 12 | 13 | it "sets appropriate headers for the response" do 14 | app 15 | req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000; includeSubDomains" 16 | end 17 | 18 | it "supports :preload option" do 19 | app(preload: true) 20 | req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000; includeSubDomains; preload" 21 | end 22 | 23 | it "supports subdomains: false option" do 24 | app(subdomains: false) 25 | req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/plugin/indifferent_params_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "indifferent_params plugin" do 4 | it "allows indifferent access to request params via params method" do 5 | app(:indifferent_params) do |r| 6 | r.on do 7 | "#{params[:a]}/#{params[:b][0][:c]}" 8 | end 9 | end 10 | 11 | body('QUERY_STRING'=>'a=2&b[][c]=3', 'rack.input'=>rack_input).must_equal '2/3' 12 | body('REQUEST_METHOD'=>'POST', 'rack.input'=>rack_input('a=2&b[][c]=3')).must_equal '2/3' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/plugin/inject_erb_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | begin 4 | require 'tilt/erb' 5 | rescue LoadError 6 | warn "tilt not installed, skipping inject_erb plugin test" 7 | else 8 | describe "inject_erb plugin" do 9 | before do 10 | app(:bare) do 11 | plugin :render, :views => './spec/views' 12 | plugin :inject_erb 13 | 14 | route do |r| 15 | r.root do 16 | render(:inline => "<% inject_erb('foo') %>") 17 | end 18 | r.get 'to_s' do 19 | render(:inline => "<% inject_erb(1) %>") 20 | end 21 | r.get 'inject' do 22 | render(:inline => "<% some_method do %>foo<% end %>") 23 | end 24 | end 25 | 26 | def some_method 27 | inject_erb "bar" 28 | yield 29 | inject_erb "baz" 30 | end 31 | end 32 | end 33 | 34 | it "should allow injecting into erb template" do 35 | body.strip.must_equal "foo" 36 | end 37 | 38 | it "should convert argument to string" do 39 | body('/to_s').strip.must_equal "1" 40 | end 41 | 42 | it "should work with the inject_erb plugin" do 43 | body('/inject').strip.must_equal "barfoobaz" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/plugin/link_to_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "link_to plugin" do 4 | it "support string values" do 5 | app(:bare) do 6 | plugin :link_to 7 | route{|r| link_to('a', '/b')} 8 | end 9 | body.must_equal 'a' 10 | end 11 | 12 | it "support symbol values for named paths" do 13 | app(:bare) do 14 | plugin :link_to 15 | path :b, '/bar' 16 | route{|r| link_to('a', :b)} 17 | end 18 | body.must_equal 'a' 19 | end 20 | 21 | it "support instances for class paths" do 22 | c = Class.new 23 | app(:bare) do 24 | plugin :link_to 25 | path c do '/bar' end 26 | route{|r| link_to('a', c.new)} 27 | end 28 | body.must_equal 'a' 29 | end 30 | 31 | it "supports nil text values to use the same as the link" do 32 | app(:bare) do 33 | plugin :link_to 34 | route{|r| link_to(nil, '/b')} 35 | end 36 | body.must_equal '/b' 37 | end 38 | 39 | it "escapes paths but not body" do 40 | app(:bare) do 41 | plugin :link_to 42 | route{|r| link_to('x', '/foo?bar=baz&q=1')} 43 | end 44 | body.must_equal 'x' 45 | end 46 | 47 | it "supports HTML options" do 48 | app(:bare) do 49 | plugin :link_to 50 | route{|r| link_to('a', '/b', 'foo'=>'"bar"', :baz=>1)} 51 | end 52 | body.must_equal 'a' 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/plugin/match_affix_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "match_affix plugin" do 4 | it "allows changing the match prefix/suffix" do 5 | app(:bare) do 6 | plugin :match_affix, "", /(\/|\z)/ 7 | 8 | route do |r| 9 | r.on "/albums" do |b| 10 | r.on "b/:id" do |id, s| 11 | "b-#{b}-#{id}-#{s.inspect}" 12 | end 13 | 14 | "albums-#{b}" 15 | end 16 | end 17 | end 18 | 19 | body("/albums/a/1").must_equal 'albums-/' 20 | body("/albums/b/1").must_equal 'b-/-1-""' 21 | end 22 | 23 | it "handles extra trailing slash only" do 24 | app(:bare) do 25 | plugin :match_affix, nil, /(?:\/\z|(?=\/|\z))/ 26 | 27 | route do |r| 28 | r.on "albums" do 29 | r.on "b" do 30 | "albums/b:#{r.remaining_path}" 31 | end 32 | 33 | "albums:#{r.remaining_path}" 34 | end 35 | end 36 | end 37 | 38 | body("/albums/a").must_equal 'albums:/a' 39 | body("/albums/a/").must_equal 'albums:/a/' 40 | body("/albums/b").must_equal 'albums/b:' 41 | body("/albums/b/").must_equal 'albums/b:' 42 | end 43 | 44 | it "allows changing the match prefix without suffix" do 45 | app(:bare) do 46 | plugin :match_affix, "" 47 | 48 | route do |r| 49 | r.on "/albums" do 50 | r.on "/b" do 51 | "albums-b-#{r.remaining_path}" 52 | end 53 | 54 | "albums-#{r.remaining_path}" 55 | end 56 | end 57 | end 58 | 59 | body("/albums/a/1").must_equal 'albums-/a/1' 60 | body("/albums/b/1").must_equal 'albums-b-/1' 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/plugin/module_include_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "module_include plugin" do 4 | it "must_include given module in request or response class" do 5 | app(:bare) do 6 | plugin :module_include 7 | request_module(Module.new{def h; halt response.finish end}) 8 | response_module(Module.new{def finish; [212, {}, []] end}) 9 | 10 | route do |r| 11 | r.h 12 | end 13 | end 14 | 15 | req.must_equal [212, {}, []] 16 | end 17 | 18 | it "should accept blocks and turn them into modules" do 19 | app(:bare) do 20 | plugin :module_include 21 | request_module{def h; halt response.finish end} 22 | response_module{def finish; [212, {}, []] end} 23 | 24 | route do |r| 25 | r.h 26 | end 27 | end 28 | 29 | req.must_equal [212, {}, []] 30 | end 31 | 32 | it "should work if called multiple times with a block" do 33 | app(:bare) do 34 | plugin :module_include 35 | request_module{def h; halt response.f end} 36 | request_module{def i; h end} 37 | response_module{def f; finish end} 38 | response_module{def finish; [212, {}, []] end} 39 | 40 | route do |r| 41 | r.i 42 | end 43 | end 44 | 45 | req.must_equal [212, {}, []] 46 | end 47 | 48 | it "should not allow both blocks and modules to be passed in single call" do 49 | app(:bare){} 50 | @app.plugin :module_include 51 | proc{@app.request_module(Module.new){}}.must_raise Roda::RodaError 52 | end 53 | 54 | it "allows calling without block or module" do 55 | app(:bare){} 56 | @app.plugin :module_include 57 | @app.request_module 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/plugin/multi_view_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | begin 4 | require 'tilt/erb' 5 | rescue LoadError 6 | warn "tilt not installed, skipping multi_view plugin test" 7 | else 8 | describe "multi_view plugin" do 9 | before do 10 | app(:bare) do 11 | plugin :render, :views=>'spec/views', :layout=>'layout-yield' 12 | plugin :multi_view 13 | 14 | route do |r| 15 | r.multi_view(['a', 'b', 'c']) 16 | end 17 | end 18 | end 19 | 20 | it "supports easy rendering of multiple views by name" do 21 | body('/a').gsub(/\s+/, '').must_equal "HeaderaFooter" 22 | body('/b').gsub(/\s+/, '').must_equal "HeaderbFooter" 23 | body('/c').gsub(/\s+/, '').must_equal "HeadercFooter" 24 | status('/d').must_equal 404 25 | status('/a', 'REQUEST_METHOD'=>'POST').must_equal 404 26 | end 27 | end 28 | 29 | describe "multi_view plugin multi_view_compile method " do 30 | before do 31 | app(:bare) do 32 | plugin :render, :views=>'spec/views', :layout=>'layout-yield' 33 | plugin :multi_view 34 | regexp = multi_view_compile(['a', 'b', 'c']) 35 | 36 | route do |r| 37 | r.multi_view(regexp) 38 | end 39 | end 40 | end 41 | 42 | it "supports easy rendering of multiple views by name" do 43 | body('/a').gsub(/\s+/, '').must_equal "HeaderaFooter" 44 | body('/b').gsub(/\s+/, '').must_equal "HeaderbFooter" 45 | body('/c').gsub(/\s+/, '').must_equal "HeadercFooter" 46 | status('/d').must_equal 404 47 | status('/a', 'REQUEST_METHOD'=>'POST').must_equal 404 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/plugin/multibyte_string_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "multibyte_string_matcher plugin" do 4 | it "uses multibyte safe string matching" do 5 | str = "\xD0\xB8".dup.force_encoding('UTF-8') 6 | app(:unescape_path) do |r| 7 | r.is String do |s| 8 | s 9 | end 10 | 11 | r.is(Integer, /(#{str})/u) do |_, a| 12 | a 13 | end 14 | 15 | r.is(Integer, Integer, str) do 16 | 'm' 17 | end 18 | 19 | r.is(Integer, str, Integer) do 20 | 'n' 21 | end 22 | end 23 | 24 | body('/%D0%B8').must_equal str 25 | body('/1/%D0%B8').must_equal str 26 | status('/1/%D0%B82').must_equal 404 27 | status('/1/2/%D0%B8').must_equal 404 28 | status('/1/%D0%B8/2').must_equal 404 29 | 30 | status('/1/%D0%B9').must_equal 404 31 | status('/1/2/%D0%B9').must_equal 404 32 | status('/1/%D0%B9/2').must_equal 404 33 | 34 | @app.plugin :multibyte_string_matcher 35 | 36 | body('/%D0%B8').must_equal str 37 | body('/1/%D0%B8').must_equal str 38 | body('/1/2/%D0%B8').must_equal 'm' 39 | body('/1/%D0%B8/2').must_equal 'n' 40 | 41 | status('/1/%D0%B82').must_equal 404 42 | status('/1/%D0%B9').must_equal 404 43 | status('/1/2/%D0%B9').must_equal 404 44 | status('/1/%D0%B9/2').must_equal 404 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/plugin/named_routes_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "named_routes plugin" do 4 | before do 5 | app(:bare) do 6 | plugin :named_routes 7 | 8 | route(:p) do |r| 9 | r.is do 10 | 'p' 11 | end 12 | end 13 | 14 | route(:q, :b) do |r| 15 | r.is do 16 | 'q' 17 | end 18 | end 19 | 20 | route do |r| 21 | r.on "p" do 22 | r.route(:p) 23 | end 24 | 25 | r.on "q" do 26 | r.route(:q, :b) 27 | end 28 | end 29 | end 30 | end 31 | 32 | it "adds named routing support" do 33 | body('/p').must_equal 'p' 34 | body('/q').must_equal 'q' 35 | 36 | status('/').must_equal 404 37 | status('/b').must_equal 404 38 | status('/p/').must_equal 404 39 | status('/q/a').must_equal 404 40 | end 41 | 42 | it "works when freezing the app" do 43 | app.freeze 44 | body('/p').must_equal 'p' 45 | body('/q').must_equal 'q' 46 | 47 | status('/').must_equal 404 48 | status('/b').must_equal 404 49 | status('/p/').must_equal 404 50 | status('/q/a').must_equal 404 51 | end 52 | 53 | it "works when subclassing the app" do 54 | @app = Class.new(@app) 55 | body('/p').must_equal 'p' 56 | body('/q').must_equal 'q' 57 | 58 | status('/').must_equal 404 59 | status('/b').must_equal 404 60 | status('/p/').must_equal 404 61 | status('/q/a').must_equal 404 62 | end 63 | 64 | it "allows removing a hash branch" do 65 | status('/p').must_equal 200 66 | 2.times do 67 | app.route(:p) 68 | proc{status('/p')}.must_raise TypeError 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/plugin/optimized_segment_matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "optimized_segment_matchers plugin" do 4 | it "should support on_segment and is_segment match methods" do 5 | app(:optimized_segment_matchers) do |r| 6 | r.on_segment do |a| 7 | r.is_segment do |b| 8 | "x-#{a}-#{b}" 9 | end 10 | 11 | "y-#{a}" 12 | end 13 | 14 | "r" 15 | end 16 | 17 | unless_lint do 18 | body('a').must_equal 'r' 19 | end 20 | body.must_equal 'r' 21 | body('/a').must_equal 'y-a' 22 | body('/a/').must_equal 'y-a' 23 | body('/b').must_equal 'y-b' 24 | body('/b/').must_equal 'y-b' 25 | body('/a/b').must_equal 'x-a-b' 26 | body('/b/a').must_equal 'x-b-a' 27 | body('/a/b/').must_equal 'y-a' 28 | body('/a/b/c').must_equal 'y-a' 29 | body('/a/b/c/').must_equal 'y-a' 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/plugin/optimized_string_matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "optimized_string_matchers plugin" do 4 | it "should support on_branch and is_exactly match methods" do 5 | app(:optimized_string_matchers) do |r| 6 | r.on_branch "e" do 7 | r.is_exactly "f" do 8 | "ef" 9 | end 10 | 11 | "ee" 12 | end 13 | 14 | r.on_branch "a" do 15 | r.on_branch "b" do 16 | r.is_exactly "c" do 17 | "c" 18 | end 19 | 20 | "b" 21 | end 22 | 23 | "a" 24 | end 25 | 26 | "cc" 27 | end 28 | 29 | body.must_equal 'cc' 30 | body('/a').must_equal 'a' 31 | body('/a/').must_equal 'a' 32 | body('/a/b/').must_equal 'b' 33 | body('/a/b/c').must_equal 'c' 34 | body('/a/b/c/').must_equal 'b' 35 | body('/a/b/c/d').must_equal 'b' 36 | body('/e').must_equal 'ee' 37 | body('/eb').must_equal 'cc' 38 | body('/e/').must_equal 'ee' 39 | body('/e/f').must_equal 'ef' 40 | body('/e/f/').must_equal 'ee' 41 | body('/e/fe').must_equal 'ee' 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/plugin/padrino_render_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | begin 4 | require 'tilt/erb' 5 | rescue LoadError 6 | warn "tilt not installed, skipping padrino_render plugin test" 7 | else 8 | describe "padrino_render plugin" do 9 | before do 10 | app(:bare) do 11 | plugin :padrino_render, :views=>"./spec/views" 12 | 13 | route do |r| 14 | 15 | r.is "render" do 16 | render(:content=>'bar', :layout_opts=>{:locals=>{:title=>"Home"}}) 17 | end 18 | 19 | r.is "render/nolayout" do 20 | render("about", :locals=>{:title => "No Layout"}, :layout=>nil) 21 | end 22 | end 23 | end 24 | end 25 | 26 | it "render uses layout by default" do 27 | body("/render").strip.must_equal "Roda: Home\nbar" 28 | end 29 | 30 | it "render doesn't use layout if layout is nil" do 31 | body("/render/nolayout").strip.must_equal "

No Layout

" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/plugin/params_capturing_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "params_capturing plugin" do 4 | it "should add captures to r.params for symbol matchers" do 5 | app(:params_capturing) do |r| 6 | r.on('foo', :y, :z, :w) do |y, z, w| 7 | (r.params.values_at('y', 'z', 'w') + [y, z, w, r.params['captures'].length]).join('-') 8 | end 9 | 10 | r.on(/(quux)/, /(foo)(bar)/) do |q, foo, bar| 11 | "y-#{r.params['captures'].join}-#{q}-#{foo}-#{bar}" 12 | end 13 | 14 | r.on(/(quux)/, :y) do |q, y| 15 | r.on(:x) do |x| 16 | "y-#{r.params['y']}-#{r.params['x']}-#{q}-#{y}-#{x}-#{r.params['captures'].length}" 17 | end 18 | 19 | "y-#{r.params['y']}-#{q}-#{y}-#{r.params['captures'].length}" 20 | end 21 | 22 | r.on('z') do 23 | r.send(:match, :y) ? 'yes' : 'no' 24 | end 25 | 26 | r.on(:x) do |x| 27 | "x-#{x}-#{r.params['x']}-#{r.params['captures'].length}" 28 | end 29 | end 30 | 31 | body('/blarg', 'rack.input'=>rack_input).must_equal 'x-blarg-blarg-1' 32 | body('/foo/1/2/3', 'rack.input'=>rack_input).must_equal '1-2-3-1-2-3-3' 33 | body('/quux/foobar', 'rack.input'=>rack_input).must_equal 'y-quuxfoobar-quux-foo-bar' 34 | body('/quux/asdf', 'rack.input'=>rack_input).must_equal 'y--quux-asdf-2' 35 | body('/quux/asdf/890', 'rack.input'=>rack_input).must_equal 'y--890-quux-asdf-890-3' 36 | body('/z', 'rack.input'=>rack_input).must_equal 'no' 37 | body('/z/x', 'rack.input'=>rack_input).must_equal 'yes' 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/plugin/pass_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "pass plugin" do 4 | it "skips the current block if pass is called" do 5 | app(:pass) do |r| 6 | r.root do 7 | r.pass if env['FOO'] == 'true' 8 | 'root' 9 | end 10 | 11 | r.on :id do |id| 12 | r.pass if id == 'foo' 13 | id 14 | end 15 | 16 | r.on :x, :y do |x, y| 17 | x + y 18 | end 19 | end 20 | 21 | body.must_equal 'root' 22 | status('FOO'=>'true').must_equal 404 23 | body("/a").must_equal 'a' 24 | body("/a/b").must_equal 'a' 25 | body("/foo/a").must_equal 'fooa' 26 | body("/foo/a/b").must_equal 'fooa' 27 | status("/foo").must_equal 404 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/plugin/path_matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "path_matchers plugin" do 4 | it ":extension matcher should match given file extension" do 5 | app(:path_matchers) do |r| 6 | r.on "css" do 7 | r.on :extension=>"css" do |file| 8 | file 9 | end 10 | end 11 | end 12 | 13 | body("/css/reset.css").must_equal 'reset' 14 | status("/css/reset.bar").must_equal 404 15 | end 16 | 17 | it ":suffix matcher should match given suffix" do 18 | app(:path_matchers) do |r| 19 | r.on "css" do 20 | r.on :suffix=>".css" do |file| 21 | file 22 | end 23 | end 24 | end 25 | 26 | body("/css/reset.css").must_equal 'reset' 27 | status("/css/reset.bar").must_equal 404 28 | end 29 | 30 | it ":prefix matcher should match given prefix" do 31 | app(:path_matchers) do |r| 32 | r.on "css" do 33 | r.on :prefix=>"reset" do |file| 34 | file 35 | end 36 | end 37 | end 38 | 39 | body("/css/reset.css").must_equal '.css' 40 | status("/css/foo.bar").must_equal 404 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/plugin/plain_hash_response_headers_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "plain_hash_response_headers plugin" do 4 | it "uses plain hashes for response headers" do 5 | app(:plain_hash_response_headers) do |r| 6 | r.get 'up' do 7 | response.headers['UP'] = 'U' 8 | end 9 | 10 | response.headers['down'] = 'd' 11 | end 12 | 13 | if Rack.release >= '3' && ENV['LINT'] 14 | proc{req('/up')}.must_raise Rack::Lint::LintError 15 | else 16 | header('up', '/up').must_be_nil 17 | header('UP', '/up').must_equal 'U' 18 | end 19 | header('down').must_equal 'd' 20 | header('DOWN').must_be_nil 21 | req[1].must_be_instance_of Hash 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/plugin/r_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "r plugin" do 4 | it "adds r method for request access" do 5 | app(:r) do |_| 6 | r.get "foo" do 7 | "foo" 8 | end 9 | 10 | "root" 11 | end 12 | 13 | body.must_equal 'root' 14 | body("/foo").must_equal 'foo' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/plugin/request_headers_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "request_headers plugin" do 4 | def header_app(header_name) 5 | app(:bare) do 6 | plugin :request_headers 7 | route do |r| 8 | r.on do 9 | # return the value of the request header in the response body, 10 | # or the static string 'not found' if it hasn't been supplied. 11 | r.headers[header_name] || 'not found' 12 | end 13 | end 14 | end 15 | end 16 | 17 | it "must add HTTP_ prefix when appropriate" do 18 | header_app('Foo') 19 | body('/', {'HTTP_FOO' => 'a'}).must_equal 'a' 20 | end 21 | 22 | it "must ignore HTTP_ prefix when appropriate" do 23 | header_app('Content-Type') 24 | body('/', {'CONTENT_TYPE' => 'a'}).must_equal 'a' 25 | end 26 | 27 | it "must return nil for non-existant headers" do 28 | header_app('X-Non-Existant') 29 | body('/').must_equal 'not found' 30 | end 31 | 32 | it "must be case-insensitive" do 33 | header_app('X-My-Header') 34 | body('/', {'HTTP_X_MY_HEADER' => 'a'}).must_equal 'a' 35 | 36 | header_app('x-my-header') 37 | body('/', {'HTTP_X_MY_HEADER' => 'a'}).must_equal 'a' 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/plugin/response_request_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "response_request plugin" do 4 | it "gives the response access to the request" do 5 | app(:response_request) do 6 | response.request.post? ? "b" : "a" 7 | end 8 | 9 | body.must_equal "a" 10 | body('REQUEST_METHOD'=>'POST').must_equal "b" 11 | end 12 | 13 | it "should work with error_handler plugin" do 14 | app(:bare) do 15 | plugin :response_request 16 | 17 | plugin :error_handler do |_| 18 | response.request.post? ? "b" : "a" 19 | end 20 | 21 | route{raise} 22 | end 23 | 24 | body.must_equal "a" 25 | body('REQUEST_METHOD'=>'POST').must_equal "b" 26 | end 27 | 28 | it "should work with class_level_routing plugin" do 29 | app(:bare) do 30 | plugin :response_request 31 | plugin :class_level_routing 32 | 33 | is '' do |_| 34 | response.request.post? ? "b" : "a" 35 | end 36 | 37 | route{} 38 | end 39 | 40 | body.must_equal "a" 41 | body('REQUEST_METHOD'=>'POST').must_equal "b" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/plugin/run_require_slash_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "run_require_slash plugin" do 4 | before do 5 | sub = app do |r| 6 | "sub-#{r.remaining_path}" 7 | end 8 | 9 | app(:bare) do 10 | plugin :match_affix, "", /(\/|\z)/ 11 | plugin :run_require_slash 12 | 13 | route do |r| 14 | r.on "/a" do |b| 15 | r.on "b" do |id, s| 16 | r.run sub 17 | "b-#{r.remaining_path}" 18 | end 19 | 20 | "albums-#{b}" 21 | end 22 | end 23 | end 24 | 25 | end 26 | 27 | it "dispatches to application for empty PATH_INFO" do 28 | body("/a/b").must_equal 'sub-' 29 | body("/a/b/").must_equal 'sub-' 30 | end unless ENV['LINT'] 31 | 32 | it "dispatches to application for PATH_INFO starting with /" do 33 | body("/a/b//").must_equal 'sub-/' 34 | end 35 | 36 | it "does not dispatch to application for PATH_INFO not starting with /" do 37 | body("/a/b/1").must_equal 'b-1' 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/plugin/shared_vars_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "shared_vars plugin" do 4 | it "adds shared method for sharing variables across multiple apps" do 5 | app(:shared_vars) {|r| shared[:c]} 6 | old_app = app 7 | app(:shared_vars) do |r| 8 | shared[:c] = 'c' 9 | r.run old_app 10 | end 11 | 12 | body.must_equal 'c' 13 | end 14 | 15 | it "adds shared with hash merges the hash into the shared vars" do 16 | app(:shared_vars) do |r| 17 | shared(:c=>'c') 18 | shared[:c] 19 | end 20 | 21 | body.must_equal 'c' 22 | end 23 | 24 | it "calling shared with hash and a block sets shared variables only for that block" do 25 | app(:shared_vars) do |r| 26 | c = nil 27 | d = nil 28 | shared[:c] = 'b' 29 | shared(:c=>'c', :d=>'d') do 30 | c = shared[:c] 31 | d = shared[:d] 32 | end 33 | "#{shared[:c]}:#{shared[:d]}:#{c}:#{d}" 34 | end 35 | 36 | body.must_equal 'b::c:d' 37 | end 38 | 39 | it "calling shared with no arguments and a block raises an error" do 40 | app(:shared_vars) do |r| 41 | shared{} 42 | end 43 | proc{body}.must_raise(Roda::RodaError) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/plugin/slash_path_empty_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "slash_path_empty" do 4 | it "considers a / path as empty" do 5 | app(:slash_path_empty) do |r| 6 | r.is{"1"} 7 | r.is("a"){"2"} 8 | r.get("b"){"3"} 9 | end 10 | 11 | unless_lint do 12 | body("").must_equal '1' 13 | body("a").must_equal '' 14 | body("b").must_equal '' 15 | end 16 | body.must_equal '1' 17 | body("/a").must_equal '2' 18 | body("/a/").must_equal '2' 19 | body("/a/b").must_equal '' 20 | body("/b").must_equal '3' 21 | body("/b/").must_equal '3' 22 | body("/b/c").must_equal '' 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/plugin/static_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "static plugin" do 4 | it "adds support for serving static files" do 5 | app(:bare) do 6 | plugin :static, ['/about'], :root=>'spec/views' 7 | 8 | route do 9 | 'a' 10 | end 11 | end 12 | 13 | body.must_equal 'a' 14 | body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') 15 | end 16 | 17 | it "respects the application's :root option" do 18 | app(:bare) do 19 | opts[:root] = File.expand_path('../../', __FILE__) 20 | plugin :static, ['/about'], :root=>'views' 21 | 22 | route do 23 | 'a' 24 | end 25 | end 26 | 27 | body.must_equal 'a' 28 | body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/plugin/status_303_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "status_303 plugin" do 4 | it 'uses a 302 for get requests' do 5 | app(:status_303) do 6 | request.redirect '/foo' 7 | fail 'redirect should halt' 8 | end 9 | status.must_equal 302 10 | body.must_equal '' 11 | header(RodaResponseHeaders::LOCATION).must_equal '/foo' 12 | end 13 | 14 | it 'uses the code given when specified' do 15 | app(:status_303) do 16 | request.redirect '/foo', 301 17 | end 18 | status.must_equal 301 19 | end 20 | 21 | it 'uses 303 for post requests if request is HTTP 1.1, 302 for 1.0' do 22 | app(:status_303) do 23 | request.redirect '/foo' 24 | end 25 | status('HTTP_VERSION' => 'HTTP/1.1', 'REQUEST_METHOD'=>'POST').must_equal 303 26 | status('HTTP_VERSION' => 'HTTP/1.0', 'REQUEST_METHOD'=>'POST').must_equal 302 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/plugin/strip_path_prefix_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "strip_path_prefix plugin" do 4 | it "strips path prefix when expanding paths" do 5 | app(:bare){} 6 | abs_dir = app.expand_path('spec') 7 | 8 | app.plugin :strip_path_prefix 9 | app.expand_path('spec').must_equal 'spec' 10 | File.expand_path(app.expand_path('spec'), Dir.pwd).must_equal abs_dir 11 | 12 | app.expand_path('/foo').must_equal '/foo' 13 | app.expand_path('bar', '/foo').must_equal '/foo/bar' 14 | 15 | app.opts[:root] = '/foo' 16 | app.expand_path('bar').must_equal '/foo/bar' 17 | app.plugin :strip_path_prefix, '/foo' 18 | app.expand_path('bar').must_equal 'bar' 19 | 20 | app.opts[:root] = '/foo/bar' 21 | app.expand_path('baz').must_equal 'bar/baz' 22 | 23 | app(:bare){} 24 | app.opts[:root] = '/foo' 25 | app.expand_path('bar').must_equal '/foo/bar' 26 | app.plugin :strip_path_prefix, '/foo/' 27 | app.expand_path('bar').must_equal 'bar' 28 | 29 | app.opts[:root] = '/foo/bar' 30 | app.expand_path('baz').must_equal 'bar/baz' 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/plugin/symbol_status_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "symbol_status plugin" do 4 | it "accepts a symbol" do 5 | app(:symbol_status) do |r| 6 | r.on do 7 | response.status = :unauthorized 8 | nil 9 | end 10 | end 11 | 12 | status.must_equal 401 13 | end 14 | 15 | it "accepts a fixnum" do 16 | app(:symbol_status) do |r| 17 | r.on do 18 | response.status = 204 19 | nil 20 | end 21 | end 22 | 23 | status.must_equal 204 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/plugin/symbol_views_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "symbol_views plugin" do 4 | before do 5 | app(:bare) do 6 | plugin :symbol_views 7 | 8 | def view(s) 9 | "v#{s}" 10 | end 11 | 12 | route do |r| 13 | r.root do 14 | :sym 15 | end 16 | 17 | r.is "string" do 18 | 'string' 19 | end 20 | end 21 | end 22 | end 23 | 24 | it "should call view with the symbol" do 25 | body.must_equal "vsym" 26 | end 27 | 28 | it "should not affect other return types" do 29 | body("/string").must_equal 'string' 30 | body("/foo").must_equal '' 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/plugin/unescape_path_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | describe "unescape_path_path plugin" do 4 | it "decodes URL-encoded routing path" do 5 | app(:unescape_path) do |r| 6 | r.on 'b' do 7 | r.get(/(.)/) do |a| 8 | "#{a}-b" 9 | end 10 | end 11 | 12 | r.get :name do |name| 13 | name 14 | end 15 | 16 | "#{r.matched_path}|#{r.remaining_path}" 17 | end 18 | 19 | body('/foo/%61').must_equal '|/foo/a' 20 | body('/a').must_equal 'a' 21 | body('/%61').must_equal 'a' 22 | unless_lint do 23 | body('%2f%61').must_equal 'a' 24 | body('%2f%62%2f%61').must_equal 'a-b' 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/redirect_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "redirects" do 4 | it "should be immediately processed" do 5 | app do |r| 6 | r.root do 7 | r.redirect "/hello" 8 | "Foo" 9 | end 10 | 11 | r.is "about" do 12 | r.redirect "/hello", 301 13 | "Foo" 14 | end 15 | 16 | r.is 'foo' do 17 | r.get do 18 | r.redirect 19 | end 20 | 21 | r.post do 22 | r.redirect 23 | end 24 | end 25 | end 26 | 27 | status.must_equal 302 28 | header(RodaResponseHeaders::LOCATION).must_equal '/hello' 29 | body.must_equal '' 30 | 31 | status("/about").must_equal 301 32 | header(RodaResponseHeaders::LOCATION, "/about").must_equal '/hello' 33 | body("/about").must_equal '' 34 | 35 | status("/foo", 'REQUEST_METHOD'=>'POST').must_equal 302 36 | header(RodaResponseHeaders::LOCATION, "/foo", 'REQUEST_METHOD'=>'POST').must_equal '/foo' 37 | body("/foo", 'REQUEST_METHOD'=>'POST').must_equal '' 38 | 39 | proc{req('/foo')}.must_raise(Roda::RodaError) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/route_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "Roda.route" do 4 | it "should set the route block" do 5 | pr = proc{'123'} 6 | app.route(&pr) 7 | app.route_block.must_equal pr 8 | body.must_equal '123' 9 | end 10 | 11 | it "should work if called in subclass and parent class later frozen" do 12 | a = app 13 | @app = Class.new(a) 14 | @app.route{|r| "OK"} 15 | body.must_equal "OK" 16 | a.freeze 17 | body.must_equal "OK" 18 | app.freeze 19 | body.must_equal "OK" 20 | end 21 | 22 | deprecated "should support #call being overridden" do 23 | app.class_eval do 24 | def call; super end 25 | end 26 | app.route{'123'} 27 | body.must_equal '123' 28 | end 29 | 30 | deprecated "should support #_call" do 31 | pr = proc{env['PATH_INFO']} 32 | app{_call(&pr)} 33 | body.must_equal '/' 34 | end 35 | 36 | deprecated "should be callable without a block" do 37 | app.route.must_be_nil 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/session_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "session handling" do 4 | include CookieJar 5 | 6 | it "should give a warning if session variable is not available" do 7 | app do |r| 8 | begin 9 | session 10 | rescue Exception => e 11 | e.message 12 | end 13 | end 14 | 15 | body.must_match("You're missing a session handler, try using the sessions plugin.") 16 | end 17 | 18 | it "should return session if session middleware is used" do 19 | require 'roda/session_middleware' 20 | app(:bare) do 21 | if RUBY_VERSION >= '2.0' 22 | require 'roda/session_middleware' 23 | use RodaSessionMiddleware, :secret=>'1'*64 24 | else 25 | use Rack::Session::Cookie, :secret=>'1'*64 26 | end 27 | 28 | route do |r| 29 | r.on do 30 | (session[1] ||= 'a'.dup) << 'b' 31 | session[1] 32 | end 33 | end 34 | end 35 | 36 | _, _, b = req 37 | b.join.must_equal 'ab' 38 | _, _, b = req 39 | b.join.must_equal 'abb' 40 | _, _, b = req 41 | b.join.must_equal 'abbb' 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/version_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | describe "Roda version constants" do 4 | it "RodaVersion should be a string in x.y.z integer format" do 5 | Roda::RodaVersion.must_match(/\A\d+\.\d+\.\d+\z/) 6 | end 7 | 8 | it "Roda*Version and RodaVersionNumber should be integers" do 9 | Roda::RodaMajorVersion.must_be_kind_of(Integer) 10 | Roda::RodaMinorVersion.must_be_kind_of(Integer) 11 | Roda::RodaPatchVersion.must_be_kind_of(Integer) 12 | Roda::RodaVersionNumber.must_be_kind_of(Integer) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/views/_each.foo.str: -------------------------------------------------------------------------------- 1 | y-#{each if local_variables.include?(:each)}-#{foo if local_variables.include?(:foo)} 2 | -------------------------------------------------------------------------------- /spec/views/_each.str: -------------------------------------------------------------------------------- 1 | x-#{each if local_variables.include?(:each)}-#{foo if local_variables.include?(:foo)} 2 | -------------------------------------------------------------------------------- /spec/views/_test.erb: -------------------------------------------------------------------------------- 1 |

<%= title %>

2 | -------------------------------------------------------------------------------- /spec/views/a.erb: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /spec/views/a.rdoc: -------------------------------------------------------------------------------- 1 | # a 2 | # * b 3 | -------------------------------------------------------------------------------- /spec/views/a1.str: -------------------------------------------------------------------------------- 1 | a#{1}-str 2 | -------------------------------------------------------------------------------- /spec/views/a2.html: -------------------------------------------------------------------------------- 1 | a2-html 2 | -------------------------------------------------------------------------------- /spec/views/a3.erb: -------------------------------------------------------------------------------- 1 | a<%= 3 %>-erb 2 | -------------------------------------------------------------------------------- /spec/views/about.erb: -------------------------------------------------------------------------------- 1 |

<%= title %>

2 | -------------------------------------------------------------------------------- /spec/views/about.str: -------------------------------------------------------------------------------- 1 |

#{title}

2 | -------------------------------------------------------------------------------- /spec/views/about/_test.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/about/_test.css.gz -------------------------------------------------------------------------------- /spec/views/about/_test.erb: -------------------------------------------------------------------------------- 1 |

Subdir: <%= title %>

2 | -------------------------------------------------------------------------------- /spec/views/about/_test.erb.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/about/_test.erb.gz -------------------------------------------------------------------------------- /spec/views/about/_test2.css: -------------------------------------------------------------------------------- 1 | body { background-color: cyan; } 2 | -------------------------------------------------------------------------------- /spec/views/about/_test2.css.br: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/about/_test2.css.br -------------------------------------------------------------------------------- /spec/views/about/_test2.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/about/_test2.css.gz -------------------------------------------------------------------------------- /spec/views/about/_test2.css.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/about/_test2.css.zst -------------------------------------------------------------------------------- /spec/views/about/comp_test.erb: -------------------------------------------------------------------------------- 1 | about-ct 2 | -------------------------------------------------------------------------------- /spec/views/about/nested/comp_test.erb: -------------------------------------------------------------------------------- 1 | about-nested-ct 2 | -------------------------------------------------------------------------------- /spec/views/about/only.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/about/only.erb -------------------------------------------------------------------------------- /spec/views/about/only_about.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/about/only_about.erb -------------------------------------------------------------------------------- /spec/views/additional/only.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/additional/only.erb -------------------------------------------------------------------------------- /spec/views/additional/only_add.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/spec/views/additional/only_add.erb -------------------------------------------------------------------------------- /spec/views/b.erb: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /spec/views/c.erb: -------------------------------------------------------------------------------- 1 | c 2 | -------------------------------------------------------------------------------- /spec/views/comp_layout.erb: -------------------------------------------------------------------------------- 1 | a<%= yield %>b 2 | -------------------------------------------------------------------------------- /spec/views/comp_test.erb: -------------------------------------------------------------------------------- 1 | ct 2 | -------------------------------------------------------------------------------- /spec/views/content-yield.erb: -------------------------------------------------------------------------------- 1 | This is the actual content. 2 | -------------------------------------------------------------------------------- /spec/views/each.foo.str: -------------------------------------------------------------------------------- 1 | r-#{each if local_variables.include?(:each)}-#{foo if local_variables.include?(:foo)} 2 | -------------------------------------------------------------------------------- /spec/views/each.str: -------------------------------------------------------------------------------- 1 | r-#{each if local_variables.include?(:each)}-#{foo if local_variables.include?(:foo)} 2 | -------------------------------------------------------------------------------- /spec/views/fixed/comp_each_test.erb: -------------------------------------------------------------------------------- 1 | <%# locals: (comp_each_test:) %>ct 2 | -------------------------------------------------------------------------------- /spec/views/fixed/comp_test.erb: -------------------------------------------------------------------------------- 1 | <%# locals: () %>ct 2 | -------------------------------------------------------------------------------- /spec/views/fixed/layout.erb: -------------------------------------------------------------------------------- 1 | <%# locals: (title:) %> 2 | Roda: <%= title %> 3 | <%= yield %> 4 | -------------------------------------------------------------------------------- /spec/views/fixed/local_test.erb: -------------------------------------------------------------------------------- 1 | <%# locals: (title:, local_test: nil) %><%= title %> 2 | -------------------------------------------------------------------------------- /spec/views/fixed/opt_local_test.erb: -------------------------------------------------------------------------------- 1 | <%# locals: (title: 'ct', opt_local_test: nil) %><%= title %> 2 | -------------------------------------------------------------------------------- /spec/views/home.erb: -------------------------------------------------------------------------------- 1 |

Home

2 |

Hello <%= name %>

3 | -------------------------------------------------------------------------------- /spec/views/home.str: -------------------------------------------------------------------------------- 1 |

Home

2 |

Hello #{name}

3 | -------------------------------------------------------------------------------- /spec/views/iv.erb: -------------------------------------------------------------------------------- 1 | <%= @a %> 2 | -------------------------------------------------------------------------------- /spec/views/layout-alternative.erb: -------------------------------------------------------------------------------- 1 | Alternative Layout: <%= title %> 2 | <%= yield %> 3 | -------------------------------------------------------------------------------- /spec/views/layout-yield.erb: -------------------------------------------------------------------------------- 1 | Header 2 | <%= yield %> 3 | Footer 4 | -------------------------------------------------------------------------------- /spec/views/layout-yield2.erb: -------------------------------------------------------------------------------- 1 | Header2 2 | <%= yield %> 3 | Footer2 4 | -------------------------------------------------------------------------------- /spec/views/layout.erb: -------------------------------------------------------------------------------- 1 | Roda: <%= title %> 2 | <%= yield %> 3 | -------------------------------------------------------------------------------- /spec/views/layout.str: -------------------------------------------------------------------------------- 1 | Roda: #{ title } 2 | #{ yield } 3 | -------------------------------------------------------------------------------- /spec/views/multiple-layout.erb: -------------------------------------------------------------------------------- 1 | <%= title %>:<%= a %>::<%= yield %> 2 | -------------------------------------------------------------------------------- /spec/views/multiple.erb: -------------------------------------------------------------------------------- 1 | <%= title %>:<%= b %> 2 | -------------------------------------------------------------------------------- /www/config.ru: -------------------------------------------------------------------------------- 1 | use Rack::Static, :urls=>%w'/index.html /why.html /documentation.html /development.html /compare-to-sinatra.html /css /rdoc /images /js', :root=>'public' 2 | run proc{[302, {'Content-Type'=>'text/html', 'Location'=>'index.html'}, []]} 3 | -------------------------------------------------------------------------------- /www/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= "#{current_page.capitalize} | " unless current_page == 'index' %>Roda: Routing Tree Web Toolkit 8 | 9 | 10 | 11 | 12 | 13 | <% unless current_page == 'index' %> 14 | 22 | <% end %> 23 |
<%= content %>
24 | 25 | 26 | -------------------------------------------------------------------------------- /www/make_www.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'erb' 3 | require_relative '../lib/roda/version' 4 | 5 | descriptions = { 6 | index: "Roda is a lightweight and productive framework for building web applications in Ruby.", 7 | documentation: "Documentation and Tutorials for Roda, the Routing Tree Web Toolkit for Ruby", 8 | development: "Contributing to Roda, the Routing Tree Web Toolkit for Ruby", 9 | compare_to_sinatra: "A brief breakdown of how Roda stacks up against Sinatra for web development.", 10 | } 11 | 12 | Dir.chdir(File.dirname(__FILE__)) 13 | erb = ERB.new(File.read('layout.erb')) 14 | Dir['pages/*.erb'].each do |page| 15 | public_loc = "#{page.gsub(/\Apages\//, 'public/').sub('.erb', '.html')}" 16 | content = content = ERB.new(File.read(page)).result(binding) 17 | current_page = File.basename(page.sub('.erb', '')) 18 | description = description = descriptions[current_page.to_sym] 19 | File.open(public_loc, 'wb'){|f| f.write(erb.result(binding))} 20 | end 21 | 22 | 23 | -------------------------------------------------------------------------------- /www/pages/development.erb: -------------------------------------------------------------------------------- 1 |
2 |

Development

3 | 4 |

Roda is under active development. You can join in on the discussions, ask questions, suggest features, and discuss Roda in general by joining the ruby-roda Google Group.

5 | 6 |

Reporting Bugs

7 | 8 |

To report a bug in Roda, use GitHub Issues. If you aren't sure if something is a bug, post a question on GitHub Discussions or the Google Group. Note that GitHub Issues should not be used to ask questions about how to use Roda; use GitHub Discussions or the Google Group for that.

9 | 10 |

Source Code

11 | 12 |

The master source code repository is jeremyevans/roda on GitHub.

13 | 14 |

Submitting Patches

15 | 16 |

The easiest way to contribute is to use Git, post the changes to a public repository, and send a pull request, either via GitHub, or the Google Group. Posting patches to the bug tracker or the Google Group works fine as well.

17 | 18 |

License

19 | 20 |

Roda is distributed under the MIT License. Patches are assumed to be submitted under the same license as Roda.

21 | 22 |
23 | -------------------------------------------------------------------------------- /www/public/data/other/memory.txt: -------------------------------------------------------------------------------- 1 | app,10,100,1000,10000 2 | roda,16100,16300,20952,62696 3 | syro,15556,15908,20320,71136 4 | hanami-api,20264,20540,24720,74156 5 | cuba,16020,16232,20572,66044 6 | -------------------------------------------------------------------------------- /www/public/data/other/rps.txt: -------------------------------------------------------------------------------- 1 | app,10,100,1000,10000 2 | roda,81021.94598078776,52503.119986517435,37978.02251781939,28577.55710634508 3 | syro,75279.23125981349,48490.643734127465,35574.57908025959,27194.450948456186 4 | hanami-api,37906.95310329373,28146.120859601764,21362.539580256405,15982.288735577406 5 | cuba,11978.091352460731,7051.030176888738,5089.749345765066,3771.809783996468 6 | -------------------------------------------------------------------------------- /www/public/data/popular/memory.txt: -------------------------------------------------------------------------------- 1 | app,10,100,1000,10000 2 | roda,15856,16424,20768,62988 3 | sinatra,27436,27776,39472,142276 4 | rails,53052,53788,58748,131560 5 | hanami,22064,25656,50476,290016 6 | -------------------------------------------------------------------------------- /www/public/data/popular/rps.txt: -------------------------------------------------------------------------------- 1 | app,10,100,1000,10000 2 | roda,81897.63532611361,51926.95155830795,38141.35070547055,28543.534354564392 3 | sinatra,5058.07626641554,2902.774036185821,592.6469838957286,59.28003578359572 4 | rails,970.9965532736129,943.9953878088751,907.8607165541686,764.2877488894684 5 | hanami,7403.360610369675,6742.304891849171,5900.963791819346,4808.113698816461 6 | -------------------------------------------------------------------------------- /www/public/images/other/memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/www/public/images/other/memory.png -------------------------------------------------------------------------------- /www/public/images/other/rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/www/public/images/other/rps.png -------------------------------------------------------------------------------- /www/public/images/popular/memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/www/public/images/popular/memory.png -------------------------------------------------------------------------------- /www/public/images/popular/rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/www/public/images/popular/rps.png -------------------------------------------------------------------------------- /www/public/images/sinatra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/roda/46aec13aa982ab191b88e902d7f2f1853c55625b/www/public/images/sinatra.png --------------------------------------------------------------------------------